UI: fixed some ui issues
Build and Publish / Build and Publish Docker Image (push) Successful in 3m23s
Build and Publish / Build and Publish Docker Image (push) Successful in 3m23s
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.2.10"
|
version = "0.2.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
||||||
|
|
||||||
|
|||||||
+201
-6
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use cot::db::{Database, Model};
|
use cot::db::{Database, Model};
|
||||||
use cot::html::Html;
|
use cot::html::Html;
|
||||||
@@ -150,6 +151,7 @@ struct AdminDashboardDto {
|
|||||||
user: AdminUserDto,
|
user: AdminUserDto,
|
||||||
build: BuildDto,
|
build: BuildDto,
|
||||||
stats: OverviewStatsDto,
|
stats: OverviewStatsDto,
|
||||||
|
runtime: RuntimeOverviewDto,
|
||||||
reviews: ReviewPageDto,
|
reviews: ReviewPageDto,
|
||||||
jobs: Vec<JobDto>,
|
jobs: Vec<JobDto>,
|
||||||
recent_runs: Vec<JobRunDto>,
|
recent_runs: Vec<JobRunDto>,
|
||||||
@@ -167,6 +169,40 @@ struct OverviewStatsDto {
|
|||||||
hidden_artists: i64,
|
hidden_artists: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
struct RuntimeOverviewDto {
|
||||||
|
agent: AgentStatusDto,
|
||||||
|
storage: Vec<StoragePathDto>,
|
||||||
|
node: NodeStatsDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
struct AgentStatusDto {
|
||||||
|
status: String,
|
||||||
|
enabled: bool,
|
||||||
|
llm_configured: bool,
|
||||||
|
model: String,
|
||||||
|
concurrency: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
struct StoragePathDto {
|
||||||
|
label: String,
|
||||||
|
path: String,
|
||||||
|
exists: bool,
|
||||||
|
free_bytes: Option<u64>,
|
||||||
|
total_bytes: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
struct NodeStatsDto {
|
||||||
|
hostname: String,
|
||||||
|
os: &'static str,
|
||||||
|
arch: &'static str,
|
||||||
|
pid: u32,
|
||||||
|
cpu_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
struct StatusCountDto {
|
struct StatusCountDto {
|
||||||
status: String,
|
status: String,
|
||||||
@@ -565,6 +601,8 @@ pub async fn dashboard(
|
|||||||
limit: Some(80),
|
limit: Some(80),
|
||||||
offset: Some(0),
|
offset: Some(0),
|
||||||
};
|
};
|
||||||
|
let (config, _) = AppConfig::load_with_db(&db).await;
|
||||||
|
let runtime = load_runtime_overview(&config);
|
||||||
let (reviews, stats, jobs, recent_runs, library) = tokio::try_join!(
|
let (reviews, stats, jobs, recent_runs, library) = tokio::try_join!(
|
||||||
load_review_page(pool, reviews_query),
|
load_review_page(pool, reviews_query),
|
||||||
load_overview_stats(pool),
|
load_overview_stats(pool),
|
||||||
@@ -582,6 +620,7 @@ pub async fn dashboard(
|
|||||||
},
|
},
|
||||||
build: build_dto(),
|
build: build_dto(),
|
||||||
stats,
|
stats,
|
||||||
|
runtime,
|
||||||
reviews,
|
reviews,
|
||||||
jobs,
|
jobs,
|
||||||
recent_runs,
|
recent_runs,
|
||||||
@@ -965,7 +1004,9 @@ pub async fn run_metadata_backfill(
|
|||||||
let duration_ms = start.elapsed().as_millis() as i64;
|
let duration_ms = start.elapsed().as_millis() as i64;
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = run.set_completed(&db_for_task, duration_ms, &log.output()).await;
|
let _ = run
|
||||||
|
.set_completed(&db_for_task, duration_ms, &log.output())
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = run
|
let _ = run
|
||||||
@@ -1233,11 +1274,13 @@ pub async fn update_library_item(
|
|||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sqlx::query("DELETE FROM furumusic__track_artist WHERE track_id = $1 AND role = 'main'")
|
sqlx::query(
|
||||||
.bind(body.id)
|
"DELETE FROM furumusic__track_artist WHERE track_id = $1 AND role = 'main'",
|
||||||
.execute(pool)
|
)
|
||||||
.await
|
.bind(body.id)
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
for (position, artist_id) in artist_ids.iter().enumerate() {
|
for (position, artist_id) in artist_ids.iter().enumerate() {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO furumusic__track_artist (track_id, artist_id, role, position) VALUES ($1, $2, 'main', $3)",
|
"INSERT INTO furumusic__track_artist (track_id, artist_id, role, position) VALUES ($1, $2, 'main', $3)",
|
||||||
@@ -1511,6 +1554,158 @@ async fn load_overview_stats(pool: &PgPool) -> anyhow::Result<OverviewStatsDto>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_runtime_overview(config: &AppConfig) -> RuntimeOverviewDto {
|
||||||
|
let llm_configured = !config.agent_llm_url.trim().is_empty();
|
||||||
|
let agent_status = if !config.agent_enabled {
|
||||||
|
"disabled"
|
||||||
|
} else if !llm_configured {
|
||||||
|
"not_configured"
|
||||||
|
} else {
|
||||||
|
"enabled"
|
||||||
|
};
|
||||||
|
|
||||||
|
RuntimeOverviewDto {
|
||||||
|
agent: AgentStatusDto {
|
||||||
|
status: agent_status.to_owned(),
|
||||||
|
enabled: config.agent_enabled,
|
||||||
|
llm_configured,
|
||||||
|
model: config.agent_llm_model.clone(),
|
||||||
|
concurrency: config.agent_concurrency,
|
||||||
|
},
|
||||||
|
storage: vec![
|
||||||
|
storage_path_dto("Inbox", &config.agent_inbox_dir),
|
||||||
|
storage_path_dto("Library", &config.agent_storage_dir),
|
||||||
|
],
|
||||||
|
node: NodeStatsDto {
|
||||||
|
hostname: node_hostname(),
|
||||||
|
os: std::env::consts::OS,
|
||||||
|
arch: std::env::consts::ARCH,
|
||||||
|
pid: std::process::id(),
|
||||||
|
cpu_count: std::thread::available_parallelism()
|
||||||
|
.map(|count| count.get())
|
||||||
|
.unwrap_or(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn storage_path_dto(label: &str, raw_path: &str) -> StoragePathDto {
|
||||||
|
let path = raw_path.trim();
|
||||||
|
let path_ref = Path::new(path);
|
||||||
|
let usage = if path.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
disk_usage(path_ref).or_else(|| {
|
||||||
|
path_ref
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.and_then(disk_usage)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
StoragePathDto {
|
||||||
|
label: label.to_owned(),
|
||||||
|
path: path.to_owned(),
|
||||||
|
exists: !path.is_empty() && path_ref.exists(),
|
||||||
|
free_bytes: usage.map(|value| value.free_bytes),
|
||||||
|
total_bytes: usage.map(|value| value.total_bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_hostname() -> String {
|
||||||
|
std::env::var("HOSTNAME")
|
||||||
|
.or_else(|_| std::env::var("COMPUTERNAME"))
|
||||||
|
.unwrap_or_else(|_| "unknown".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct DiskUsage {
|
||||||
|
free_bytes: u64,
|
||||||
|
total_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn disk_usage(path: &Path) -> Option<DiskUsage> {
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
|
||||||
|
#[link(name = "kernel32")]
|
||||||
|
unsafe extern "system" {
|
||||||
|
fn GetDiskFreeSpaceExW(
|
||||||
|
lpDirectoryName: *const u16,
|
||||||
|
lpFreeBytesAvailableToCaller: *mut u64,
|
||||||
|
lpTotalNumberOfBytes: *mut u64,
|
||||||
|
lpTotalNumberOfFreeBytes: *mut u64,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
|
||||||
|
wide.push(0);
|
||||||
|
|
||||||
|
let mut free_available = 0_u64;
|
||||||
|
let mut total = 0_u64;
|
||||||
|
let mut total_free = 0_u64;
|
||||||
|
let ok = unsafe {
|
||||||
|
GetDiskFreeSpaceExW(
|
||||||
|
wide.as_ptr(),
|
||||||
|
&mut free_available,
|
||||||
|
&mut total,
|
||||||
|
&mut total_free,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(ok != 0).then_some(DiskUsage {
|
||||||
|
free_bytes: free_available,
|
||||||
|
total_bytes: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
fn disk_usage(path: &Path) -> Option<DiskUsage> {
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct Statvfs {
|
||||||
|
f_bsize: std::ffi::c_ulong,
|
||||||
|
f_frsize: std::ffi::c_ulong,
|
||||||
|
f_blocks: std::ffi::c_ulong,
|
||||||
|
f_bfree: std::ffi::c_ulong,
|
||||||
|
f_bavail: std::ffi::c_ulong,
|
||||||
|
f_files: std::ffi::c_ulong,
|
||||||
|
f_ffree: std::ffi::c_ulong,
|
||||||
|
f_favail: std::ffi::c_ulong,
|
||||||
|
f_fsid: std::ffi::c_ulong,
|
||||||
|
f_flag: std::ffi::c_ulong,
|
||||||
|
f_namemax: std::ffi::c_ulong,
|
||||||
|
__f_spare: [std::ffi::c_int; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn statvfs(path: *const std::ffi::c_char, buf: *mut Statvfs) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
|
||||||
|
let mut stat = std::mem::MaybeUninit::<Statvfs>::uninit();
|
||||||
|
let ok = unsafe { statvfs(c_path.as_ptr(), stat.as_mut_ptr()) };
|
||||||
|
if ok != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stat = unsafe { stat.assume_init() };
|
||||||
|
let fragment_size = if stat.f_frsize > 0 {
|
||||||
|
stat.f_frsize as u64
|
||||||
|
} else {
|
||||||
|
stat.f_bsize as u64
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(DiskUsage {
|
||||||
|
free_bytes: stat.f_bavail as u64 * fragment_size,
|
||||||
|
total_bytes: stat.f_blocks as u64 * fragment_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(windows, target_os = "linux", target_os = "android")))]
|
||||||
|
fn disk_usage(_path: &Path) -> Option<DiskUsage> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
async fn load_library_overview(pool: &PgPool) -> anyhow::Result<LibraryOverviewDto> {
|
async fn load_library_overview(pool: &PgPool) -> anyhow::Result<LibraryOverviewDto> {
|
||||||
let stats = load_overview_stats(pool).await?;
|
let stats = load_overview_stats(pool).await?;
|
||||||
Ok(LibraryOverviewDto {
|
Ok(LibraryOverviewDto {
|
||||||
|
|||||||
+53
-6
@@ -185,7 +185,7 @@ button {
|
|||||||
|
|
||||||
.stats-strip {
|
.stats-strip {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(104px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@@ -201,12 +201,18 @@ button {
|
|||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: var(--text-subdued);
|
color: var(--text-subdued);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@@ -1355,8 +1361,8 @@ tbody tr:hover {
|
|||||||
<div class="content" x-show="activeView === 'reviews'">
|
<div class="content" x-show="activeView === 'reviews'">
|
||||||
<section class="stats-strip">
|
<section class="stats-strip">
|
||||||
<template x-for="cell in statCells()" :key="cell.label">
|
<template x-for="cell in statCells()" :key="cell.label">
|
||||||
<div class="stat-cell">
|
<div class="stat-cell" :title="cell.title || ''">
|
||||||
<div class="stat-value" x-text="fmt(cell.value)"></div>
|
<div class="stat-value" x-text="cell.display || fmt(cell.value)"></div>
|
||||||
<div class="stat-label" x-text="cell.label"></div>
|
<div class="stat-label" x-text="cell.label"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2298,6 +2304,7 @@ function adminV2() {
|
|||||||
libraryLoading: false,
|
libraryLoading: false,
|
||||||
toastMessage: '',
|
toastMessage: '',
|
||||||
stats: {},
|
stats: {},
|
||||||
|
runtime: { agent: {}, storage: [], node: {} },
|
||||||
libraryOverview: {},
|
libraryOverview: {},
|
||||||
reviews: { items: [], total: 0, limit: 80, offset: 0, status_counts: [] },
|
reviews: { items: [], total: 0, limit: 80, offset: 0, status_counts: [] },
|
||||||
reviewFilter: { status: null, search: '' },
|
reviewFilter: { status: null, search: '' },
|
||||||
@@ -2419,6 +2426,7 @@ function adminV2() {
|
|||||||
try {
|
try {
|
||||||
const data = await this.request(`${this.apiBase}/dashboard`);
|
const data = await this.request(`${this.apiBase}/dashboard`);
|
||||||
this.stats = data.stats || {};
|
this.stats = data.stats || {};
|
||||||
|
this.runtime = data.runtime || this.runtime;
|
||||||
this.libraryOverview = data.library || {};
|
this.libraryOverview = data.library || {};
|
||||||
this.reviews = data.reviews || this.reviews;
|
this.reviews = data.reviews || this.reviews;
|
||||||
this.jobs = data.jobs || [];
|
this.jobs = data.jobs || [];
|
||||||
@@ -3406,17 +3414,56 @@ function adminV2() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
statCells() {
|
statCells() {
|
||||||
|
const agent = (this.runtime && this.runtime.agent) || {};
|
||||||
|
const storage = (this.runtime && this.runtime.storage) || [];
|
||||||
|
const node = (this.runtime && this.runtime.node) || {};
|
||||||
|
const inbox = storage.find(item => item.label === 'Inbox') || {};
|
||||||
|
const library = storage.find(item => item.label === 'Library') || {};
|
||||||
return [
|
return [
|
||||||
{ label: 'Tracks', value: this.stats.tracks || 0 },
|
{ label: 'Tracks', value: this.stats.tracks || 0 },
|
||||||
{ label: 'Releases', value: this.stats.releases || 0 },
|
{ label: 'Releases', value: this.stats.releases || 0 },
|
||||||
{ label: 'Artists', value: this.stats.artists || 0 },
|
{ label: 'Artists', value: this.stats.artists || 0 },
|
||||||
{ label: 'Playlists', value: this.stats.playlists || 0 },
|
{ label: 'Playlists', value: this.stats.playlists || 0 },
|
||||||
{ label: 'Hidden tracks', value: this.stats.hidden_tracks || 0 },
|
{
|
||||||
{ label: 'Hidden releases', value: this.stats.hidden_releases || 0 },
|
label: agent.model ? `AI agent · ${agent.model}` : 'AI agent',
|
||||||
{ label: 'Hidden artists', value: this.stats.hidden_artists || 0 }
|
display: agent.status || 'unknown',
|
||||||
|
title: `enabled: ${agent.enabled ? 'yes' : 'no'} · llm: ${agent.llm_configured ? 'configured' : 'missing'} · concurrency: ${agent.concurrency || 0}`
|
||||||
|
},
|
||||||
|
this.storageStatCell(inbox, 'Inbox disk'),
|
||||||
|
this.storageStatCell(library, 'Library disk'),
|
||||||
|
{
|
||||||
|
label: `${node.hostname || 'node'} · pid ${node.pid || '?'}`,
|
||||||
|
display: `${node.cpu_count || '?'} CPU`,
|
||||||
|
title: `${node.os || 'unknown'} / ${node.arch || 'unknown'}`
|
||||||
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
storageStatCell(item, fallbackLabel) {
|
||||||
|
const free = item && item.free_bytes != null ? item.free_bytes : null;
|
||||||
|
const total = item && item.total_bytes != null ? item.total_bytes : null;
|
||||||
|
const suffix = item && item.exists === false ? ' · missing' : '';
|
||||||
|
return {
|
||||||
|
label: `${item.label || fallbackLabel}${suffix}`,
|
||||||
|
display: free != null ? `${this.formatBytes(free)} free` : 'n/a',
|
||||||
|
title: `${item.path || 'not configured'}${total != null ? ` · ${this.formatBytes(total)} total` : ''}`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
formatBytes(bytes) {
|
||||||
|
const value = Number(bytes || 0);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
let size = value;
|
||||||
|
let unit = 0;
|
||||||
|
while (size >= 1024 && unit < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unit += 1;
|
||||||
|
}
|
||||||
|
const digits = size >= 10 || unit === 0 ? 0 : 1;
|
||||||
|
return `${size.toFixed(digits)} ${units[unit]}`;
|
||||||
|
},
|
||||||
|
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
if (this.activeView === 'library') return 'Library Workbench';
|
if (this.activeView === 'library') return 'Library Workbench';
|
||||||
if (this.activeView === 'jobs') return 'Tasks';
|
if (this.activeView === 'jobs') return 'Tasks';
|
||||||
|
|||||||
+13
-114
@@ -168,18 +168,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
Alpine.store('mobile', {
|
Alpine.store('mobile', {
|
||||||
libraryOpen: false,
|
libraryOpen: false,
|
||||||
playerExpanded: false,
|
playerExpanded: false,
|
||||||
playerDragging: false,
|
|
||||||
playerDragOffset: 0,
|
|
||||||
playerCloseOffset: 0,
|
|
||||||
_playerDragStartY: 0,
|
|
||||||
_playerDragStartX: 0,
|
|
||||||
_playerDragTracking: false,
|
|
||||||
_playerDragMode: null,
|
|
||||||
_playerDragPointerId: null,
|
|
||||||
_playerDragElement: null,
|
|
||||||
_playerDragMove: null,
|
|
||||||
_playerDragEnd: null,
|
|
||||||
_playerSuppressClickUntil: 0,
|
|
||||||
toggleLibrary() {
|
toggleLibrary() {
|
||||||
this.libraryOpen = !this.libraryOpen;
|
this.libraryOpen = !this.libraryOpen;
|
||||||
if (this.libraryOpen) Alpine.store('user').menuOpen = false;
|
if (this.libraryOpen) Alpine.store('user').menuOpen = false;
|
||||||
@@ -188,119 +176,30 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.libraryOpen = false;
|
this.libraryOpen = false;
|
||||||
},
|
},
|
||||||
isMobilePlayer() {
|
isMobilePlayer() {
|
||||||
return window.matchMedia && window.matchMedia('(max-width: 720px)').matches;
|
if (!window.matchMedia) return false;
|
||||||
|
return window.matchMedia('(max-width: 900px)').matches
|
||||||
|
|| window.matchMedia('(pointer: coarse) and (max-width: 1024px)').matches;
|
||||||
},
|
},
|
||||||
openPlayerFullscreen() {
|
openPlayerFullscreen() {
|
||||||
if (!this.isMobilePlayer() || !Alpine.store('player').currentTrack) return;
|
if (!this.isMobilePlayer() || !Alpine.store('player').currentTrack) return;
|
||||||
this.playerExpanded = true;
|
this.playerExpanded = true;
|
||||||
this.playerDragging = false;
|
|
||||||
this.playerDragOffset = 0;
|
|
||||||
this.playerCloseOffset = 0;
|
|
||||||
Alpine.store('queue').visible = false;
|
Alpine.store('queue').visible = false;
|
||||||
Alpine.store('devices').open = false;
|
Alpine.store('devices').open = false;
|
||||||
|
this.resetPlayerFullscreenScroll();
|
||||||
},
|
},
|
||||||
closePlayerFullscreen() {
|
closePlayerFullscreen() {
|
||||||
this.playerExpanded = false;
|
this.playerExpanded = false;
|
||||||
this.playerDragging = false;
|
Alpine.store('queue').visible = false;
|
||||||
this.playerDragOffset = 0;
|
Alpine.store('devices').open = false;
|
||||||
this.playerCloseOffset = 0;
|
|
||||||
},
|
},
|
||||||
playerDragStyle() {
|
resetPlayerFullscreenScroll() {
|
||||||
return `--mobile-player-drag:${this.playerDragOffset}px; --mobile-player-close-drag:${this.playerCloseOffset}px;`;
|
requestAnimationFrame(() => {
|
||||||
},
|
const full = document.querySelector('.player-bar .mobile-full-player');
|
||||||
handlePlayerClick(event) {
|
if (full) {
|
||||||
if (Date.now() <= this._playerSuppressClickUntil) {
|
full.scrollTop = 0;
|
||||||
event.preventDefault();
|
full.scrollLeft = 0;
|
||||||
event.stopPropagation();
|
|
||||||
this._playerSuppressClickUntil = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startPlayerDrag(event, force = false) {
|
|
||||||
if (!this.isMobilePlayer() || !Alpine.store('player').currentTrack) return;
|
|
||||||
if (event.button && event.button !== 0) return;
|
|
||||||
const target = event.target;
|
|
||||||
const isDragBlocked = target.closest('input, select, textarea, .volume-slider, .device-popover, .mobile-expanded-queue');
|
|
||||||
if (!force) {
|
|
||||||
if (this.playerExpanded) {
|
|
||||||
const isCloseHandle = target.closest('.player-now-playing');
|
|
||||||
const scroller = event.currentTarget?.classList?.contains('player-bar') ? event.currentTarget : null;
|
|
||||||
if (!isCloseHandle || isDragBlocked || (scroller && scroller.scrollTop > 4)) return;
|
|
||||||
} else if (isDragBlocked) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
if (this._playerDragTracking) this.endPlayerDrag({ type: 'pointercancel' });
|
|
||||||
this.playerDragging = false;
|
|
||||||
this._playerDragTracking = true;
|
|
||||||
this._playerDragMode = this.playerExpanded ? 'close' : 'open';
|
|
||||||
this._playerDragStartY = event.clientY;
|
|
||||||
this._playerDragStartX = event.clientX;
|
|
||||||
this._playerDragPointerId = event.pointerId;
|
|
||||||
this._playerDragElement = event.currentTarget;
|
|
||||||
this.playerDragOffset = 0;
|
|
||||||
this.playerCloseOffset = 0;
|
|
||||||
this._playerDragMove = e => this.movePlayerDrag(e);
|
|
||||||
this._playerDragEnd = e => this.endPlayerDrag(e);
|
|
||||||
window.addEventListener('pointermove', this._playerDragMove, { passive: false });
|
|
||||||
window.addEventListener('pointerup', this._playerDragEnd, { passive: false });
|
|
||||||
window.addEventListener('pointercancel', this._playerDragEnd, { passive: false });
|
|
||||||
},
|
|
||||||
movePlayerDrag(event) {
|
|
||||||
if (!this._playerDragTracking) return;
|
|
||||||
const delta = this._playerDragStartY - event.clientY;
|
|
||||||
const absDelta = Math.abs(delta);
|
|
||||||
if (!this.playerDragging) {
|
|
||||||
const horizontalDelta = Math.abs(event.clientX - this._playerDragStartX);
|
|
||||||
const wantsOpen = this._playerDragMode === 'open' && delta > 0;
|
|
||||||
const wantsClose = this._playerDragMode === 'close' && delta < 0;
|
|
||||||
if (absDelta < 8 || absDelta < horizontalDelta * 1.15 || (!wantsOpen && !wantsClose)) return;
|
|
||||||
this.playerDragging = true;
|
|
||||||
try {
|
|
||||||
this._playerDragElement?.setPointerCapture?.(this._playerDragPointerId);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
if (this._playerDragMode === 'close') {
|
|
||||||
this.playerCloseOffset = Math.max(0, Math.min(window.innerHeight, -delta));
|
|
||||||
} else {
|
|
||||||
const max = Math.max(0, window.innerHeight - 132);
|
|
||||||
this.playerDragOffset = Math.max(0, Math.min(max, delta));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
endPlayerDrag(event) {
|
|
||||||
const openThreshold = Math.min(180, Math.max(90, window.innerHeight * 0.18));
|
|
||||||
const closeThreshold = Math.min(110, Math.max(64, window.innerHeight * 0.1));
|
|
||||||
const wasCancelled = event?.type === 'pointercancel';
|
|
||||||
const wasDragging = this.playerDragging;
|
|
||||||
if (wasDragging) this._playerSuppressClickUntil = Date.now() + 450;
|
|
||||||
if (this._playerDragMode === 'close') {
|
|
||||||
if (!wasCancelled && this.playerCloseOffset > closeThreshold) this.closePlayerFullscreen();
|
|
||||||
else {
|
|
||||||
this.playerCloseOffset = 0;
|
|
||||||
this.playerDragging = false;
|
|
||||||
}
|
|
||||||
} else if (this._playerDragMode === 'open' && !wasCancelled && this.playerDragOffset > openThreshold) {
|
|
||||||
this.openPlayerFullscreen();
|
|
||||||
} else {
|
|
||||||
this.playerDragOffset = 0;
|
|
||||||
this.playerDragging = false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (this._playerDragPointerId !== null && this._playerDragElement?.hasPointerCapture?.(this._playerDragPointerId)) {
|
|
||||||
this._playerDragElement.releasePointerCapture(this._playerDragPointerId);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
if (this._playerDragMove) window.removeEventListener('pointermove', this._playerDragMove);
|
|
||||||
if (this._playerDragEnd) {
|
|
||||||
window.removeEventListener('pointerup', this._playerDragEnd);
|
|
||||||
window.removeEventListener('pointercancel', this._playerDragEnd);
|
|
||||||
}
|
|
||||||
this._playerDragTracking = false;
|
|
||||||
this._playerDragMode = null;
|
|
||||||
this._playerDragPointerId = null;
|
|
||||||
this._playerDragElement = null;
|
|
||||||
this._playerDragMove = null;
|
|
||||||
this._playerDragEnd = null;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+15
-16
@@ -958,16 +958,14 @@
|
|||||||
|
|
||||||
<!-- Player Bar -->
|
<!-- Player Bar -->
|
||||||
<div class="player-bar"
|
<div class="player-bar"
|
||||||
:class="{ 'mobile-expanded': $store.mobile.playerExpanded, 'mobile-dragging': $store.mobile.playerDragging }"
|
:class="{ 'mobile-expanded': $store.mobile.playerExpanded }">
|
||||||
:style="$store.mobile.playerDragStyle()"
|
<div class="mobile-full-player">
|
||||||
@click.capture="$store.mobile.handlePlayerClick($event)"
|
<button class="mobile-player-collapse-btn" type="button" @click.stop="$store.mobile.closePlayerFullscreen()" title="{{ t.player_close }}" aria-label="{{ t.player_close }}">
|
||||||
@pointerdown="$store.mobile.startPlayerDrag($event)">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
||||||
<button class="mobile-player-collapse-btn" type="button" @click.stop="$store.mobile.closePlayerFullscreen()" title="{{ t.player_close }}" aria-label="{{ t.player_close }}">
|
<path d="M6 9l6 6 6-6"/>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
</svg>
|
||||||
<path d="M6 9l6 6 6-6"/>
|
</button>
|
||||||
</svg>
|
<div class="player-now-playing">
|
||||||
</button>
|
|
||||||
<div class="player-now-playing">
|
|
||||||
<template x-if="$store.player.currentTrack">
|
<template x-if="$store.player.currentTrack">
|
||||||
<div style="display:flex;align-items:center;gap:12px;overflow:hidden">
|
<div style="display:flex;align-items:center;gap:12px;overflow:hidden">
|
||||||
<div class="player-cover"
|
<div class="player-cover"
|
||||||
@@ -1007,9 +1005,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="player-controls">
|
<div class="player-controls">
|
||||||
<div class="player-buttons">
|
<div class="player-buttons">
|
||||||
<button class="player-btn" :class="{ active: $store.player.shuffle }" @click="$store.player.toggleShuffle()" title="{{ t.player_shuffle }}">
|
<button class="player-btn" :class="{ active: $store.player.shuffle }" @click="$store.player.toggleShuffle()" title="{{ t.player_shuffle }}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
||||||
@@ -1049,9 +1047,9 @@
|
|||||||
<span class="player-time" x-text="formatTime($store.player.duration)"></span>
|
<span class="player-time" x-text="formatTime($store.player.duration)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-version-chip">v{{ t.app_version() }}</div>
|
<div class="player-version-chip">v{{ t.app_version() }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="player-right">
|
<div class="player-right">
|
||||||
<div class="volume-control">
|
<div class="volume-control">
|
||||||
<button class="volume-btn" @click="$store.player.toggleMute()">
|
<button class="volume-btn" @click="$store.player.toggleMute()">
|
||||||
<template x-if="$store.player.volume === 0">
|
<template x-if="$store.player.volume === 0">
|
||||||
@@ -1199,8 +1197,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-expanded-queue">
|
<div class="mobile-expanded-queue">
|
||||||
<div class="mobile-expanded-queue-title">{{ t.player_queue }}</div>
|
<div class="mobile-expanded-queue-title">{{ t.player_queue }}</div>
|
||||||
<template x-if="$store.queue.upcoming().length === 0">
|
<template x-if="$store.queue.upcoming().length === 0">
|
||||||
<div class="mobile-expanded-queue-empty">{{ t.player_queue_empty }}</div>
|
<div class="mobile-expanded-queue-empty">{{ t.player_queue_empty }}</div>
|
||||||
@@ -1233,5 +1231,6 @@
|
|||||||
<span class="mobile-expanded-queue-time" x-text="formatTime(track.duration_seconds)"></span>
|
<span class="mobile-expanded-queue-time" x-text="formatTime(track.duration_seconds)"></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3644,6 +3644,10 @@ button.user-stat:hover {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-full-player {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
.playlist-action-btn {
|
.playlist-action-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -3696,7 +3700,7 @@ button.user-stat:hover {
|
|||||||
.release-meta .release-title { font-size: 32px; }
|
.release-meta .release-title { font-size: 32px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px), (pointer: coarse) and (max-width: 1024px) {
|
||||||
:root {
|
:root {
|
||||||
--player-height: 168px;
|
--player-height: 168px;
|
||||||
--player-bar-space: calc(var(--player-height) + var(--safe-bottom));
|
--player-bar-space: calc(var(--player-height) + var(--safe-bottom));
|
||||||
@@ -3879,7 +3883,7 @@ button.user-stat:hover {
|
|||||||
gap: 4px 10px;
|
gap: 4px 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 34px 12px calc(9px + var(--safe-bottom));
|
padding: 34px 12px calc(9px + var(--safe-bottom));
|
||||||
touch-action: none;
|
touch-action: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4115,20 +4119,32 @@ button.user-stat:hover {
|
|||||||
height: 21px;
|
height: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-dragging:not(.mobile-expanded) {
|
.player-bar.mobile-expanded {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
height: calc(var(--player-bar-space) + var(--mobile-player-drag, 0px));
|
bottom: auto;
|
||||||
z-index: 70;
|
height: 100dvh;
|
||||||
border-radius: 18px 18px 0 0;
|
display: block;
|
||||||
box-shadow: 0 -18px 54px rgba(0,0,0,0.5);
|
padding: 0;
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
z-index: 80;
|
||||||
|
touch-action: auto;
|
||||||
|
user-select: auto;
|
||||||
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded {
|
.player-bar.mobile-expanded .mobile-full-player {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
inset: 0;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -4136,26 +4152,14 @@ button.user-stat:hover {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: calc(18px + env(safe-area-inset-top)) 18px calc(16px + var(--safe-bottom));
|
padding: calc(18px + env(safe-area-inset-top)) 18px calc(16px + var(--safe-bottom));
|
||||||
border-top: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
box-shadow: none;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
z-index: 80;
|
z-index: 3;
|
||||||
transform: translateY(var(--mobile-player-close-drag, 0px));
|
|
||||||
transition: transform 0.18s ease, height 0.18s ease;
|
|
||||||
touch-action: pan-y;
|
|
||||||
user-select: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-dragging {
|
.player-bar.mobile-expanded .mobile-full-player .mobile-player-collapse-btn {
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-bar.mobile-expanded .mobile-player-collapse-btn {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: calc(12px + env(safe-area-inset-top));
|
top: calc(12px + env(safe-area-inset-top));
|
||||||
right: 12px;
|
right: 12px;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
@@ -4174,7 +4178,7 @@ button.user-stat:hover {
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-now-playing {
|
.player-bar.mobile-expanded .mobile-full-player .player-now-playing {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -4182,11 +4186,11 @@ button.user-stat:hover {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding-top: 38px;
|
padding-top: 38px;
|
||||||
touch-action: none;
|
touch-action: auto;
|
||||||
cursor: grab;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-now-playing > div {
|
.player-bar.mobile-expanded .mobile-full-player .player-now-playing > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -4195,60 +4199,56 @@ button.user-stat:hover {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded.mobile-dragging .player-now-playing {
|
.player-bar.mobile-expanded .mobile-full-player .player-cover {
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-cover {
|
|
||||||
width: min(76vw, 38dvh, 360px);
|
width: min(76vw, 38dvh, 360px);
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 22px 62px rgba(0,0,0,0.48);
|
box-shadow: 0 22px 62px rgba(0,0,0,0.48);
|
||||||
touch-action: none;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-cover svg {
|
.player-bar.mobile-expanded .mobile-full-player .player-cover svg {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-track-info {
|
.player-bar.mobile-expanded .mobile-full-player .player-track-info {
|
||||||
width: min(100%, 520px);
|
width: min(100%, 520px);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-track-title-row {
|
.player-bar.mobile-expanded .mobile-full-player .player-track-title-row {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-track-title {
|
.player-bar.mobile-expanded .mobile-full-player .player-track-title {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-track-artist {
|
.player-bar.mobile-expanded .mobile-full-player .player-track-artist {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-current-like {
|
.player-bar.mobile-expanded .mobile-full-player .player-current-like {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-track-release {
|
.player-bar.mobile-expanded .mobile-full-player .player-track-release {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-controls {
|
.player-bar.mobile-expanded .mobile-full-player .player-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -4257,32 +4257,32 @@ button.user-stat:hover {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-version-chip {
|
.player-bar.mobile-expanded .mobile-full-player .player-version-chip {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-buttons {
|
.player-bar.mobile-expanded .mobile-full-player .player-buttons {
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-btn {
|
.player-bar.mobile-expanded .mobile-full-player .player-btn {
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-btn svg {
|
.player-bar.mobile-expanded .mobile-full-player .player-btn svg {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-btn-play {
|
.player-bar.mobile-expanded .mobile-full-player .player-btn-play {
|
||||||
width: 72px;
|
width: 72px;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-timeline {
|
.player-bar.mobile-expanded .mobile-full-player .player-timeline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
@@ -4291,16 +4291,16 @@ button.user-stat:hover {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .progress-bar {
|
.player-bar.mobile-expanded .mobile-full-player .progress-bar {
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .progress-bar-thumb {
|
.player-bar.mobile-expanded .mobile-full-player .progress-bar-thumb {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-right {
|
.player-bar.mobile-expanded .mobile-full-player .player-right {
|
||||||
position: static;
|
position: static;
|
||||||
grid-area: actions;
|
grid-area: actions;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
@@ -4313,28 +4313,28 @@ button.user-stat:hover {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .volume-control {
|
.player-bar.mobile-expanded .mobile-full-player .volume-control {
|
||||||
grid-template-columns: 44px minmax(0, 1fr);
|
grid-template-columns: 44px minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .volume-btn,
|
.player-bar.mobile-expanded .mobile-full-player .volume-btn,
|
||||||
.player-bar.mobile-expanded .queue-toggle-btn {
|
.player-bar.mobile-expanded .mobile-full-player .queue-toggle-btn {
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .volume-btn {
|
.player-bar.mobile-expanded .mobile-full-player .volume-btn {
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .volume-slider {
|
.player-bar.mobile-expanded .mobile-full-player .volume-slider {
|
||||||
display: block;
|
display: block;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .device-popover {
|
.player-bar.mobile-expanded .mobile-full-player .device-popover {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: calc(90px + var(--safe-bottom));
|
bottom: calc(90px + var(--safe-bottom));
|
||||||
@@ -4345,7 +4345,7 @@ button.user-stat:hover {
|
|||||||
max-height: 42dvh;
|
max-height: 42dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .mobile-expanded-queue {
|
.player-bar.mobile-expanded .mobile-full-player .mobile-expanded-queue {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
@@ -4991,26 +4991,26 @@ button.user-stat:hover {
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded {
|
.player-bar.mobile-expanded .mobile-full-player {
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
padding-right: 14px;
|
padding-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-cover {
|
.player-bar.mobile-expanded .mobile-full-player .player-cover {
|
||||||
width: min(82vw, 36dvh, 340px);
|
width: min(82vw, 36dvh, 340px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-buttons {
|
.player-bar.mobile-expanded .mobile-full-player .player-buttons {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-btn {
|
.player-bar.mobile-expanded .mobile-full-player .player-btn {
|
||||||
min-width: 52px;
|
min-width: 52px;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-bar.mobile-expanded .player-btn-play {
|
.player-bar.mobile-expanded .mobile-full-player .player-btn-play {
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 68px;
|
height: 68px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user