Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8073ac9a97 | |||
| ec7c5c9049 | |||
| d1113effa5 | |||
| 072c00a48e | |||
| 34e25fac2c |
Generated
+1
-1
@@ -1418,7 +1418,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.2.1"
|
version = "0.2.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.2.1"
|
version = "0.2.5"
|
||||||
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"
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,10 @@ translations! {
|
|||||||
player_lastfm_connected: "Connected as {user}" , "Подключён: {user}";
|
player_lastfm_connected: "Connected as {user}" , "Подключён: {user}";
|
||||||
player_lastfm_reconnect: "Reconnect Last.fm" , "Переподключить Last.fm";
|
player_lastfm_reconnect: "Reconnect Last.fm" , "Переподключить Last.fm";
|
||||||
player_lastfm_not_configured: "Last.fm is not configured" , "Last.fm не настроен";
|
player_lastfm_not_configured: "Last.fm is not configured" , "Last.fm не настроен";
|
||||||
|
player_lastfm_status_connect: "connect account" , "подключить аккаунт";
|
||||||
|
player_lastfm_status_connected: "connected" , "подключён";
|
||||||
|
player_lastfm_status_reconnect: "reconnect account" , "переподключить аккаунт";
|
||||||
|
player_lastfm_status_not_configured: "not configured" , "не настроен";
|
||||||
player_lastfm_disconnect_confirm: "Disconnect Last.fm account {user}?" , "Отвязать аккаунт Last.fm {user}?";
|
player_lastfm_disconnect_confirm: "Disconnect Last.fm account {user}?" , "Отвязать аккаунт Last.fm {user}?";
|
||||||
player_lastfm_connect_failed: "Could not open Last.fm connection" , "Не удалось открыть подключение Last.fm";
|
player_lastfm_connect_failed: "Could not open Last.fm connection" , "Не удалось открыть подключение Last.fm";
|
||||||
player_lastfm_disconnect_failed: "Could not disconnect Last.fm" , "Не удалось отвязать Last.fm";
|
player_lastfm_disconnect_failed: "Could not disconnect Last.fm" , "Не удалось отвязать Last.fm";
|
||||||
|
|||||||
+233
-2
@@ -40,13 +40,13 @@ pub(super) struct ArtistDetail {
|
|||||||
pub(super) featured_tracks: Vec<ArtistAppearanceTrack>,
|
pub(super) featured_tracks: Vec<ArtistAppearanceTrack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||||
pub(super) struct ArtistRef {
|
pub(super) struct ArtistRef {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
pub(super) name: String,
|
pub(super) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||||
pub(super) struct TrackItem {
|
pub(super) struct TrackItem {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
pub(super) title: String,
|
pub(super) title: String,
|
||||||
@@ -137,6 +137,125 @@ pub(super) struct PlaybackStateDto {
|
|||||||
pub(super) volume: f64,
|
pub(super) volume: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct DeviceHeartbeatRequest {
|
||||||
|
pub(super) device_id: String,
|
||||||
|
pub(super) user_agent: Option<String>,
|
||||||
|
pub(super) current_jam_id: Option<String>,
|
||||||
|
pub(super) playback_state: Option<PlayerDevicePlaybackStateDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct DeviceSelectRequest {
|
||||||
|
pub(super) device_id: String,
|
||||||
|
pub(super) current_device_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct DeviceCommandRequest {
|
||||||
|
pub(super) target_device_id: Option<String>,
|
||||||
|
pub(super) jam_id: Option<String>,
|
||||||
|
pub(super) command: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) payload: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerDeviceDto {
|
||||||
|
pub(super) id: String,
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) kind: String,
|
||||||
|
pub(super) is_current: bool,
|
||||||
|
pub(super) is_active: bool,
|
||||||
|
pub(super) last_seen_ms: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerJamDto {
|
||||||
|
pub(super) id: String,
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) host_user_id: i64,
|
||||||
|
pub(super) host_name: String,
|
||||||
|
pub(super) is_owner: bool,
|
||||||
|
pub(super) is_member: bool,
|
||||||
|
pub(super) is_pending: bool,
|
||||||
|
pub(super) is_active: bool,
|
||||||
|
pub(super) member_count: i64,
|
||||||
|
pub(super) host_last_seen_ms: i64,
|
||||||
|
pub(super) host_device_online: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerJamCreateRequest {
|
||||||
|
pub(super) device_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) invitee_user_ids: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerJamJoinRequest {
|
||||||
|
pub(super) jam_id: String,
|
||||||
|
pub(super) device_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerJamLeaveRequest {
|
||||||
|
pub(super) jam_id: String,
|
||||||
|
pub(super) device_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerJamUserDto {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) username: String,
|
||||||
|
pub(super) display_name: Option<String>,
|
||||||
|
pub(super) email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerDeviceCommandDto {
|
||||||
|
pub(super) id: String,
|
||||||
|
pub(super) command: String,
|
||||||
|
pub(super) payload: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerDevicePlaybackStateDto {
|
||||||
|
pub(super) track: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) tracks: Vec<serde_json::Value>,
|
||||||
|
pub(super) index: i32,
|
||||||
|
pub(super) position_seconds: f64,
|
||||||
|
pub(super) duration_seconds: f64,
|
||||||
|
pub(super) paused: bool,
|
||||||
|
pub(super) shuffle: bool,
|
||||||
|
pub(super) repeat_mode: String,
|
||||||
|
pub(super) volume: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) updated_at_ms: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerDevicesResponse {
|
||||||
|
pub(super) device_id: String,
|
||||||
|
pub(super) active_device_id: Option<String>,
|
||||||
|
pub(super) devices: Vec<PlayerDeviceDto>,
|
||||||
|
pub(super) jams: Vec<PlayerJamDto>,
|
||||||
|
pub(super) current_jam_id: Option<String>,
|
||||||
|
pub(super) playback_state: Option<PlayerDevicePlaybackStateDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct PlayerDevicePollResponse {
|
||||||
|
pub(super) device_id: String,
|
||||||
|
pub(super) active_device_id: Option<String>,
|
||||||
|
pub(super) devices: Vec<PlayerDeviceDto>,
|
||||||
|
pub(super) jams: Vec<PlayerJamDto>,
|
||||||
|
pub(super) current_jam_id: Option<String>,
|
||||||
|
pub(super) commands: Vec<PlayerDeviceCommandDto>,
|
||||||
|
pub(super) playback_state: Option<PlayerDevicePlaybackStateDto>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
pub(super) struct PlaylistDetail {
|
pub(super) struct PlaylistDetail {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
@@ -207,6 +326,118 @@ pub(super) struct AgentQueueStatus {
|
|||||||
pub(super) processing_count: i64,
|
pub(super) processing_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadTrack {
|
||||||
|
pub(super) track: TrackItem,
|
||||||
|
pub(super) media_file_id: i64,
|
||||||
|
pub(super) is_hidden: bool,
|
||||||
|
pub(super) release_is_hidden: bool,
|
||||||
|
pub(super) release_type: String,
|
||||||
|
pub(super) year: Option<i32>,
|
||||||
|
pub(super) uploaded_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadRelease {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) title: String,
|
||||||
|
pub(super) release_type: String,
|
||||||
|
pub(super) year: Option<i32>,
|
||||||
|
pub(super) is_hidden: bool,
|
||||||
|
pub(super) artists: Vec<ArtistRef>,
|
||||||
|
pub(super) tracks: Vec<UserUploadTrack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadReviewFields {
|
||||||
|
pub(super) title: String,
|
||||||
|
pub(super) artist: String,
|
||||||
|
pub(super) album: String,
|
||||||
|
pub(super) year: String,
|
||||||
|
pub(super) track_number: String,
|
||||||
|
pub(super) genre: String,
|
||||||
|
pub(super) featured_artists: Vec<String>,
|
||||||
|
pub(super) release_type: String,
|
||||||
|
pub(super) notes: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadReviewItem {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) status: String,
|
||||||
|
pub(super) filename: String,
|
||||||
|
pub(super) created_at: String,
|
||||||
|
pub(super) updated_at: String,
|
||||||
|
pub(super) error_message: Option<String>,
|
||||||
|
pub(super) fields: UserUploadReviewFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadQueueItem {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) status: String,
|
||||||
|
pub(super) filename: String,
|
||||||
|
pub(super) created_at: String,
|
||||||
|
pub(super) updated_at: String,
|
||||||
|
pub(super) error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadsPage {
|
||||||
|
pub(super) tracks: Vec<UserUploadTrack>,
|
||||||
|
pub(super) releases: Vec<UserUploadRelease>,
|
||||||
|
pub(super) pending: Vec<UserUploadReviewItem>,
|
||||||
|
pub(super) queued: Vec<UserUploadQueueItem>,
|
||||||
|
pub(super) pending_total: i64,
|
||||||
|
pub(super) queued_total: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadTrackUpdateRequest {
|
||||||
|
pub(super) title: Option<String>,
|
||||||
|
pub(super) artist_names: Option<Vec<String>>,
|
||||||
|
pub(super) featured_artist_names: Option<Vec<String>>,
|
||||||
|
pub(super) release_title: Option<String>,
|
||||||
|
pub(super) release_type: Option<String>,
|
||||||
|
pub(super) release_year: Option<String>,
|
||||||
|
pub(super) track_number: Option<String>,
|
||||||
|
pub(super) disc_number: Option<String>,
|
||||||
|
pub(super) is_hidden: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadReleaseUpdateRequest {
|
||||||
|
pub(super) title: Option<String>,
|
||||||
|
pub(super) artist_names: Option<Vec<String>>,
|
||||||
|
pub(super) release_type: Option<String>,
|
||||||
|
pub(super) year: Option<String>,
|
||||||
|
pub(super) is_hidden: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadBulkTrackUpdateRequest {
|
||||||
|
pub(super) track_ids: Vec<i64>,
|
||||||
|
pub(super) artist_names: Option<Vec<String>>,
|
||||||
|
pub(super) featured_artist_names: Option<Vec<String>>,
|
||||||
|
pub(super) release_title: Option<String>,
|
||||||
|
pub(super) release_type: Option<String>,
|
||||||
|
pub(super) release_year: Option<String>,
|
||||||
|
pub(super) is_hidden: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub(super) struct UserUploadReviewUpdateRequest {
|
||||||
|
pub(super) title: Option<String>,
|
||||||
|
pub(super) artist: Option<String>,
|
||||||
|
pub(super) album: Option<String>,
|
||||||
|
pub(super) year: Option<String>,
|
||||||
|
pub(super) track_number: Option<String>,
|
||||||
|
pub(super) genre: Option<String>,
|
||||||
|
pub(super) featured_artists: Option<Vec<String>>,
|
||||||
|
pub(super) release_type: Option<String>,
|
||||||
|
pub(super) notes: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
pub(super) struct PlayHistoryItem {
|
pub(super) struct PlayHistoryItem {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
|
|||||||
+2596
-3
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,11 @@ pub(super) struct TracksByIdsRequest {
|
|||||||
pub(super) ids: Vec<i64>,
|
pub(super) ids: Vec<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct UserUploadsQuery {
|
||||||
|
pub(super) limit: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(super) struct CreatePlaylistRequest {
|
pub(super) struct CreatePlaylistRequest {
|
||||||
pub(super) title: String,
|
pub(super) title: String,
|
||||||
@@ -62,6 +67,12 @@ pub(super) struct SearchQuery {
|
|||||||
pub(super) limit: Option<i32>,
|
pub(super) limit: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct JamUserSearchQuery {
|
||||||
|
pub(super) q: Option<String>,
|
||||||
|
pub(super) limit: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(super) struct PathTrackId {
|
pub(super) struct PathTrackId {
|
||||||
pub(super) track_id: i64,
|
pub(super) track_id: i64,
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ pub(super) struct CountRow {
|
|||||||
pub(super) count: i64,
|
pub(super) count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct PlayerJamUserRow {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) username: String,
|
||||||
|
pub(super) display_name: Option<String>,
|
||||||
|
pub(super) email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub(super) struct ReleaseRow {
|
pub(super) struct ReleaseRow {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
@@ -60,6 +68,13 @@ pub(super) struct TrackArtistRow {
|
|||||||
pub(super) role: String,
|
pub(super) role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct ReleaseArtistRefRow {
|
||||||
|
pub(super) release_id: i64,
|
||||||
|
pub(super) artist_id: i64,
|
||||||
|
pub(super) artist_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub(super) struct MediaFileRow {
|
pub(super) struct MediaFileRow {
|
||||||
pub(super) file_path: String,
|
pub(super) file_path: String,
|
||||||
@@ -124,6 +139,70 @@ pub(super) struct PlaylistTrackRow {
|
|||||||
pub(super) lastfm_updated_at: Option<String>,
|
pub(super) lastfm_updated_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct UploadedTrackRow {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) title: String,
|
||||||
|
pub(super) track_number: Option<i32>,
|
||||||
|
pub(super) disc_number: Option<i32>,
|
||||||
|
pub(super) duration_seconds: f64,
|
||||||
|
pub(super) cover_file_id: Option<i64>,
|
||||||
|
pub(super) release_cover_file_id: Option<i64>,
|
||||||
|
pub(super) release_id: i64,
|
||||||
|
pub(super) release_title: String,
|
||||||
|
pub(super) release_type: String,
|
||||||
|
pub(super) release_year: Option<i32>,
|
||||||
|
pub(super) release_is_hidden: bool,
|
||||||
|
pub(super) uploader_name: String,
|
||||||
|
pub(super) audio_format: Option<String>,
|
||||||
|
pub(super) audio_bitrate: Option<i32>,
|
||||||
|
pub(super) audio_sample_rate: Option<i32>,
|
||||||
|
pub(super) audio_bit_depth: Option<i32>,
|
||||||
|
pub(super) file_size_bytes: Option<i64>,
|
||||||
|
pub(super) lastfm_listeners: Option<i64>,
|
||||||
|
pub(super) lastfm_playcount: Option<i64>,
|
||||||
|
pub(super) lastfm_rating: Option<f64>,
|
||||||
|
pub(super) lastfm_updated_at: Option<String>,
|
||||||
|
pub(super) media_file_id: i64,
|
||||||
|
pub(super) is_hidden: bool,
|
||||||
|
pub(super) year: Option<i32>,
|
||||||
|
pub(super) uploaded_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct UserUploadQueueRow {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) status: String,
|
||||||
|
pub(super) input_path: Option<String>,
|
||||||
|
pub(super) created_at: String,
|
||||||
|
pub(super) updated_at: String,
|
||||||
|
pub(super) error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct UserUploadReviewRow {
|
||||||
|
pub(super) id: i64,
|
||||||
|
pub(super) status: String,
|
||||||
|
pub(super) input_path: Option<String>,
|
||||||
|
pub(super) result_json: Option<String>,
|
||||||
|
pub(super) context_json: Option<String>,
|
||||||
|
pub(super) created_at: String,
|
||||||
|
pub(super) updated_at: String,
|
||||||
|
pub(super) error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub(super) struct UploadTrackEditRow {
|
||||||
|
pub(super) release_id: i64,
|
||||||
|
pub(super) title: String,
|
||||||
|
pub(super) track_number: Option<i32>,
|
||||||
|
pub(super) disc_number: Option<i32>,
|
||||||
|
pub(super) is_hidden: bool,
|
||||||
|
pub(super) release_title: String,
|
||||||
|
pub(super) release_type: String,
|
||||||
|
pub(super) release_year: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub(super) struct AppearanceTrackRow {
|
pub(super) struct AppearanceTrackRow {
|
||||||
pub(super) id: i64,
|
pub(super) id: i64,
|
||||||
|
|||||||
@@ -112,6 +112,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="torrent-tabs">
|
||||||
|
<button class="torrent-tab-btn"
|
||||||
|
:class="{ active: $store.torrents.activeTab === 'import' }"
|
||||||
|
@click="$store.torrents.showImportTab()">Import</button>
|
||||||
|
<button class="torrent-tab-btn"
|
||||||
|
:class="{ active: $store.torrents.activeTab === 'uploads' }"
|
||||||
|
@click="$store.torrents.showUploadsTab()">
|
||||||
|
<span>My uploads</span>
|
||||||
|
<span class="torrent-tab-count"
|
||||||
|
x-show="$store.torrents.uploadPendingTotal + $store.torrents.uploadQueuedTotal > 0"
|
||||||
|
x-text="$store.torrents.uploadPendingTotal + $store.torrents.uploadQueuedTotal"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.activeTab === 'import'">
|
||||||
<div class="torrent-manager-layout">
|
<div class="torrent-manager-layout">
|
||||||
<aside class="torrent-manager-sidebar">
|
<aside class="torrent-manager-sidebar">
|
||||||
<div class="torrent-manager-title">
|
<div class="torrent-manager-title">
|
||||||
@@ -319,6 +334,272 @@
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.activeTab === 'uploads'">
|
||||||
|
<section class="upload-manager-panel">
|
||||||
|
<div class="upload-manager-head">
|
||||||
|
<div>
|
||||||
|
<h4>My uploaded tracks</h4>
|
||||||
|
<p x-text="$store.torrents.uploadSummary()"></p>
|
||||||
|
</div>
|
||||||
|
<button class="modal-btn modal-btn-ghost"
|
||||||
|
@click="$store.torrents.loadUploads()"
|
||||||
|
:disabled="$store.torrents.uploadHasEditorOpen()">{{ t.player_refresh }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadLoaded && $store.torrents.uploadTracks.length === 0 && $store.torrents.uploadPending.length === 0 && $store.torrents.uploadQueued.length === 0">
|
||||||
|
<div class="empty-state torrent-workspace-empty">
|
||||||
|
<p>No uploaded tracks yet</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="upload-manager-grid">
|
||||||
|
<aside class="upload-review-column">
|
||||||
|
<div class="upload-panel-card">
|
||||||
|
<div class="upload-panel-title">Needs approval</div>
|
||||||
|
<p class="upload-panel-subtitle" x-text="$store.torrents.uploadPendingTotal + ' pending or failed'"></p>
|
||||||
|
<template x-if="$store.torrents.uploadPending.length === 0">
|
||||||
|
<div class="upload-mini-empty">No tracks need approval</div>
|
||||||
|
</template>
|
||||||
|
<div class="upload-review-list">
|
||||||
|
<template x-for="item in $store.torrents.uploadPending" :key="item.id">
|
||||||
|
<button class="upload-review-row" :class="{ active: $store.torrents.uploadReviewEditId === item.id, failed: item.status === 'failed' }" @click="$store.torrents.editUploadReview(item)">
|
||||||
|
<span class="torrent-status-badge" :class="'status-' + item.status" x-text="item.status"></span>
|
||||||
|
<span class="upload-review-name" x-text="item.filename"></span>
|
||||||
|
<span class="upload-review-error" x-show="item.error_message" x-text="item.error_message"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template x-if="$store.torrents.uploadQueuedTotal > 0">
|
||||||
|
<div class="upload-panel-card upload-queue-panel">
|
||||||
|
<div class="upload-panel-title">Queued / processing</div>
|
||||||
|
<template x-for="item in $store.torrents.compactQueuedUploads()" :key="item.id">
|
||||||
|
<div class="upload-queue-row">
|
||||||
|
<span class="torrent-status-badge" :class="'status-' + item.status" x-text="item.status"></span>
|
||||||
|
<span class="upload-queue-name" x-text="item.filename"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="upload-mini-empty" x-show="$store.torrents.uploadQueuedTotal > $store.torrents.uploadQueued.length" x-text="'Showing ' + $store.torrents.uploadQueued.length + ' of ' + $store.torrents.uploadQueuedTotal"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</aside>
|
||||||
|
<section class="upload-library-column">
|
||||||
|
<div class="upload-bulk-bar" x-show="$store.torrents.uploadSelectedCount() > 0">
|
||||||
|
<div class="upload-bulk-title" x-text="$store.torrents.uploadSelectedCount() + ' selected'"></div>
|
||||||
|
<input type="text" placeholder="Artists" x-model="$store.torrents.uploadBulkDraft.artists">
|
||||||
|
<input type="text" placeholder="Featured" x-model="$store.torrents.uploadBulkDraft.featured_artists">
|
||||||
|
<input type="text" placeholder="Album" x-model="$store.torrents.uploadBulkDraft.release_title">
|
||||||
|
<input type="number" placeholder="Year" x-model="$store.torrents.uploadBulkDraft.release_year">
|
||||||
|
<select x-model="$store.torrents.uploadBulkDraft.release_type">
|
||||||
|
<option value="">Type unchanged</option><option value="album">Album</option><option value="single">Single</option><option value="ep">EP</option><option value="compilation">Compilation</option><option value="mixtape">Mixtape</option><option value="live">Live</option><option value="soundtrack">Soundtrack</option><option value="remix">Remix</option><option value="demo">Demo</option>
|
||||||
|
</select>
|
||||||
|
<select x-model="$store.torrents.uploadBulkDraft.hidden">
|
||||||
|
<option value="">Visibility unchanged</option><option value="false">Visible</option><option value="true">Hidden</option>
|
||||||
|
</select>
|
||||||
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadBulkEdit()" :disabled="$store.torrents.uploadBulkSaving">Apply</button>
|
||||||
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.clearUploadSelection()">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="upload-release-tree">
|
||||||
|
<template x-for="release in $store.torrents.uploadReleases" :key="release.id">
|
||||||
|
<div class="upload-release-node" :class="{ hidden: release.is_hidden }">
|
||||||
|
<div class="upload-release-row">
|
||||||
|
<button class="torrent-tree-check" :class="$store.torrents.uploadReleaseSelectionState(release)" @click="$store.torrents.toggleUploadReleaseSelection(release)">
|
||||||
|
<template x-if="$store.torrents.uploadReleaseSelectionState(release) === 'checked'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></template>
|
||||||
|
<template x-if="$store.torrents.uploadReleaseSelectionState(release) === 'partial'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="5" y1="12" x2="19" y2="12"/></svg></template>
|
||||||
|
</button>
|
||||||
|
<button class="torrent-tree-toggle" :class="{ expanded: $store.torrents.uploadReleaseExpanded(release.id) }" @click="$store.torrents.toggleUploadRelease(release.id)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>
|
||||||
|
<div class="upload-release-main">
|
||||||
|
<div class="upload-release-title"><span x-text="release.title"></span><span class="upload-hidden-pill" x-show="release.is_hidden">hidden</span></div>
|
||||||
|
<div class="upload-track-meta"><span x-text="$store.torrents.uploadReleaseArtistsText(release)"></span><span>-</span><span x-text="release.year || 'no year'"></span><span>-</span><span x-text="release.tracks.length + ' tracks'"></span></div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUploadRelease(release)">Edit release</button>
|
||||||
|
</div>
|
||||||
|
<div class="upload-track-children" x-show="$store.torrents.uploadReleaseExpanded(release.id)">
|
||||||
|
<template x-for="item in release.tracks" :key="item.track.id">
|
||||||
|
<div class="upload-tree-track" :class="{ hidden: item.is_hidden, selected: $store.torrents.selectedUploadTracks.has(item.track.id) }">
|
||||||
|
<button class="torrent-tree-check" :class="{ checked: $store.torrents.selectedUploadTracks.has(item.track.id) }" @click="$store.torrents.toggleUploadTrackSelection(item.track.id)">
|
||||||
|
<template x-if="$store.torrents.selectedUploadTracks.has(item.track.id)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></template>
|
||||||
|
</button>
|
||||||
|
<button class="track-action-btn play-btn" @click="$store.player.play(item.track)" title="{{ t.player_play }}"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button>
|
||||||
|
<div class="upload-track-main">
|
||||||
|
<div class="upload-track-title"><span x-text="item.track.track_number ? item.track.track_number + '. ' + item.track.title : item.track.title"></span><span class="upload-hidden-pill" x-show="item.is_hidden">hidden</span></div>
|
||||||
|
<div class="upload-track-meta"><span x-text="$store.torrents.uploadArtistsText(item)"></span><span x-show="$store.torrents.uploadFeaturedArtistsText(item)">feat.</span><span x-show="$store.torrents.uploadFeaturedArtistsText(item)" x-text="$store.torrents.uploadFeaturedArtistsText(item)"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-track-actions">
|
||||||
|
<button class="track-action-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg></button>
|
||||||
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUpload(item)">Edit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadHasEditorOpen()">
|
||||||
|
<div class="upload-editor-backdrop" @click.self="$store.torrents.closeUploadEditor()">
|
||||||
|
<aside class="upload-editor-drawer">
|
||||||
|
<div class="upload-editor-head">
|
||||||
|
<div>
|
||||||
|
<div class="upload-panel-title" x-text="$store.torrents.uploadEditorKicker()"></div>
|
||||||
|
<h4 x-text="$store.torrents.uploadEditorTitle()"></h4>
|
||||||
|
</div>
|
||||||
|
<button class="track-action-btn" @click="$store.torrents.closeUploadEditor()" title="Close">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadReviewDraft">
|
||||||
|
<div class="upload-editor-form">
|
||||||
|
<label class="upload-field upload-field-half"><span>Title</span><input type="text" x-model="$store.torrents.uploadReviewDraft.title"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Artist</span><input type="text" x-model="$store.torrents.uploadReviewDraft.artist"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Album</span><input type="text" x-model="$store.torrents.uploadReviewDraft.album"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Featured</span><input type="text" x-model="$store.torrents.uploadReviewDraft.featured_artists" placeholder="Artist, Artist"></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Year</span><input type="number" x-model="$store.torrents.uploadReviewDraft.year"></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Track #</span><input type="number" min="1" x-model="$store.torrents.uploadReviewDraft.track_number"></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Genre</span><input type="text" x-model="$store.torrents.uploadReviewDraft.genre"></label>
|
||||||
|
<label class="upload-field upload-field-compact">
|
||||||
|
<span>Type</span>
|
||||||
|
<select x-model="$store.torrents.uploadReviewDraft.release_type">
|
||||||
|
<option value="album">Album</option><option value="single">Single</option><option value="ep">EP</option><option value="compilation">Compilation</option><option value="mixtape">Mixtape</option><option value="live">Live</option><option value="soundtrack">Soundtrack</option><option value="remix">Remix</option><option value="demo">Demo</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="upload-field upload-field-wide"><span>Notes</span><textarea rows="4" x-model="$store.torrents.uploadReviewDraft.notes"></textarea></label>
|
||||||
|
<div class="upload-editor-actions">
|
||||||
|
<button class="modal-btn modal-btn-danger" @click="$store.torrents.deleteUploadReview()" :disabled="$store.torrents.uploadReviewSavingId === $store.torrents.uploadReviewEditId">Delete review</button>
|
||||||
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.approveUploadReview()" :disabled="$store.torrents.uploadReviewSavingId === $store.torrents.uploadReviewEditId">Approve</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadReleaseDraft">
|
||||||
|
<div class="upload-editor-form">
|
||||||
|
<label class="upload-field upload-field-wide"><span>Album</span><input type="text" x-model="$store.torrents.uploadReleaseDraft.title"></label>
|
||||||
|
<label class="upload-field upload-field-wide"><span>Album artists</span><input type="text" x-model="$store.torrents.uploadReleaseDraft.artists"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Year</span><input type="number" x-model="$store.torrents.uploadReleaseDraft.year"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Type</span><select x-model="$store.torrents.uploadReleaseDraft.release_type"><option value="album">Album</option><option value="single">Single</option><option value="ep">EP</option><option value="compilation">Compilation</option><option value="mixtape">Mixtape</option><option value="live">Live</option><option value="soundtrack">Soundtrack</option><option value="remix">Remix</option><option value="demo">Demo</option></select></label>
|
||||||
|
<label class="upload-field upload-field-toggle"><input type="checkbox" x-model="$store.torrents.uploadReleaseDraft.is_hidden"><span>Hidden</span></label>
|
||||||
|
<div class="upload-editor-actions">
|
||||||
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadReleaseEdit()" :disabled="$store.torrents.uploadReleaseSavingId === $store.torrents.uploadReleaseEditId">Save release</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadDraft">
|
||||||
|
<div class="upload-editor-form">
|
||||||
|
<label class="upload-field upload-field-wide"><span>Title</span><input type="text" x-model="$store.torrents.uploadDraft.title"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Artists</span><input type="text" x-model="$store.torrents.uploadDraft.artists"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Featured</span><input type="text" x-model="$store.torrents.uploadDraft.featured_artists"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Album</span><input type="text" x-model="$store.torrents.uploadDraft.release_title"></label>
|
||||||
|
<label class="upload-field upload-field-half"><span>Type</span><select x-model="$store.torrents.uploadDraft.release_type"><option value="album">Album</option><option value="single">Single</option><option value="ep">EP</option><option value="compilation">Compilation</option><option value="mixtape">Mixtape</option><option value="live">Live</option><option value="soundtrack">Soundtrack</option><option value="remix">Remix</option><option value="demo">Demo</option></select></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Year</span><input type="number" x-model="$store.torrents.uploadDraft.release_year"></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Track #</span><input type="number" min="1" x-model="$store.torrents.uploadDraft.track_number"></label>
|
||||||
|
<label class="upload-field upload-field-compact"><span>Disc #</span><input type="number" min="1" x-model="$store.torrents.uploadDraft.disc_number"></label>
|
||||||
|
<label class="upload-field upload-field-toggle"><input type="checkbox" x-model="$store.torrents.uploadDraft.is_hidden"><span>Hidden</span></label>
|
||||||
|
<div class="upload-editor-actions">
|
||||||
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadEdit()" :disabled="$store.torrents.uploadSavingId === $store.torrents.uploadEditId">Save track</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="upload-track-list" x-show="false">
|
||||||
|
<template x-for="item in $store.torrents.uploadTracks" :key="item.track.id">
|
||||||
|
<div class="upload-track-card" :class="{ hidden: item.is_hidden }">
|
||||||
|
<template x-if="$store.torrents.uploadEditId !== item.track.id">
|
||||||
|
<div class="upload-track-display">
|
||||||
|
<button class="track-action-btn play-btn"
|
||||||
|
@click="$store.player.play(item.track)"
|
||||||
|
title="{{ t.player_play }}">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="upload-track-main">
|
||||||
|
<div class="upload-track-title">
|
||||||
|
<span x-text="item.track.title"></span>
|
||||||
|
<span class="upload-hidden-pill" x-show="item.is_hidden">hidden</span>
|
||||||
|
</div>
|
||||||
|
<div class="upload-track-meta">
|
||||||
|
<span x-text="$store.torrents.uploadArtistsText(item)"></span>
|
||||||
|
<span>·</span>
|
||||||
|
<span x-text="item.track.release_title"></span>
|
||||||
|
<span x-show="item.track.track_number">·</span>
|
||||||
|
<span x-show="item.track.track_number" x-text="'#' + item.track.track_number"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-track-actions">
|
||||||
|
<button class="track-action-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUpload(item)">Edit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.torrents.uploadEditId === item.track.id">
|
||||||
|
<div class="upload-edit-form">
|
||||||
|
<label>
|
||||||
|
<span>Title</span>
|
||||||
|
<input type="text" x-model="$store.torrents.uploadDraft.title">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Artists</span>
|
||||||
|
<input type="text" x-model="$store.torrents.uploadDraft.artists" placeholder="Artist, Featured Artist">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Release</span>
|
||||||
|
<input type="text" x-model="$store.torrents.uploadDraft.release_title">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Type</span>
|
||||||
|
<select x-model="$store.torrents.uploadDraft.release_type">
|
||||||
|
<option value="album">Album</option>
|
||||||
|
<option value="single">Single</option>
|
||||||
|
<option value="ep">EP</option>
|
||||||
|
<option value="compilation">Compilation</option>
|
||||||
|
<option value="mixtape">Mixtape</option>
|
||||||
|
<option value="live">Live</option>
|
||||||
|
<option value="soundtrack">Soundtrack</option>
|
||||||
|
<option value="remix">Remix</option>
|
||||||
|
<option value="demo">Demo</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Year</span>
|
||||||
|
<input type="number" x-model="$store.torrents.uploadDraft.release_year">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Track #</span>
|
||||||
|
<input type="number" min="1" x-model="$store.torrents.uploadDraft.track_number">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Disc #</span>
|
||||||
|
<input type="number" min="1" x-model="$store.torrents.uploadDraft.disc_number">
|
||||||
|
</label>
|
||||||
|
<label class="upload-hidden-toggle">
|
||||||
|
<input type="checkbox" x-model="$store.torrents.uploadDraft.is_hidden">
|
||||||
|
<span>Hidden</span>
|
||||||
|
</label>
|
||||||
|
<div class="upload-edit-actions">
|
||||||
|
<button class="modal-btn modal-btn-primary"
|
||||||
|
@click="$store.torrents.saveUploadEdit()"
|
||||||
|
:disabled="$store.torrents.uploadSavingId === item.track.id">Save</button>
|
||||||
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.cancelUploadEdit()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1084
-22
File diff suppressed because it is too large
Load Diff
+127
-7
@@ -41,9 +41,15 @@
|
|||||||
<button class="lastfm-profile-action"
|
<button class="lastfm-profile-action"
|
||||||
:class="$store.user.lastfmClass()"
|
:class="$store.user.lastfmClass()"
|
||||||
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
|
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
|
||||||
@click="$store.user.handleLastfm()">
|
@click="$store.user.handleLastfm()"
|
||||||
|
:title="$store.user.lastfmLabel()"
|
||||||
|
:aria-label="$store.user.lastfmLabel()">
|
||||||
<span class="lastfm-dot"></span>
|
<span class="lastfm-dot"></span>
|
||||||
<span class="lastfm-profile-text" x-text="$store.user.lastfmLabel()"></span>
|
<span class="lastfm-profile-text">
|
||||||
|
<span class="lastfm-profile-brand">{{ t.player_lastfm_profile }}</span>
|
||||||
|
<span class="lastfm-profile-separator">·</span>
|
||||||
|
<span class="lastfm-profile-status" x-text="$store.user.lastfmStatusLabel()"></span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -348,9 +354,15 @@
|
|||||||
<button class="lastfm-profile-action"
|
<button class="lastfm-profile-action"
|
||||||
:class="$store.user.lastfmClass()"
|
:class="$store.user.lastfmClass()"
|
||||||
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
|
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
|
||||||
@click="$store.user.handleLastfm()">
|
@click="$store.user.handleLastfm()"
|
||||||
|
:title="$store.user.lastfmLabel()"
|
||||||
|
:aria-label="$store.user.lastfmLabel()">
|
||||||
<span class="lastfm-dot"></span>
|
<span class="lastfm-dot"></span>
|
||||||
<span class="lastfm-profile-text" x-text="$store.user.lastfmLabel()"></span>
|
<span class="lastfm-profile-text">
|
||||||
|
<span class="lastfm-profile-brand">{{ t.player_lastfm_profile }}</span>
|
||||||
|
<span class="lastfm-profile-separator">·</span>
|
||||||
|
<span class="lastfm-profile-status" x-text="$store.user.lastfmStatusLabel()"></span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="modal-btn modal-btn-primary mobile-account-logout"
|
<button class="modal-btn modal-btn-primary mobile-account-logout"
|
||||||
@click="$store.user.logout()">
|
@click="$store.user.logout()">
|
||||||
@@ -750,7 +762,7 @@
|
|||||||
<template x-for="(track, idx) in $store.library.currentRelease.tracks" :key="track.id">
|
<template x-for="(track, idx) in $store.library.currentRelease.tracks" :key="track.id">
|
||||||
<div class="track-row"
|
<div class="track-row"
|
||||||
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
|
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
|
||||||
@dblclick="$store.queue.playRelease($store.library.currentRelease.tracks, idx)">
|
@dblclick="$store.queue.playRelease([track], 0)">
|
||||||
<span class="track-num" x-text="track.track_number || (idx + 1)"></span>
|
<span class="track-num" x-text="track.track_number || (idx + 1)"></span>
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
<div class="track-title" x-text="track.title"></div>
|
<div class="track-title" x-text="track.title"></div>
|
||||||
@@ -774,7 +786,7 @@
|
|||||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentRelease.tracks, idx)" title="{{ t.player_play }}">
|
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease([track], 0)" title="{{ t.player_play }}">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
||||||
@@ -1038,7 +1050,7 @@
|
|||||||
<div class="volume-slider"
|
<div class="volume-slider"
|
||||||
@pointerdown.prevent="$store.player.startVolumeDrag($event)"
|
@pointerdown.prevent="$store.player.startVolumeDrag($event)"
|
||||||
aria-label="{{ t.player_volume }}">
|
aria-label="{{ t.player_volume }}">
|
||||||
<div class="volume-slider-fill" :style="'width:' + ($store.player.volume * 100) + '%'">
|
<div class="volume-slider-fill" :style="'width:' + $store.player.volumeSliderPercent() + '%'">
|
||||||
<div class="volume-slider-thumb"></div>
|
<div class="volume-slider-thumb"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1046,5 +1058,113 @@
|
|||||||
<button class="queue-toggle-btn" :class="{ active: $store.queue.visible }" @click="$store.queue.visible = !$store.queue.visible" title="{{ t.player_queue }}">
|
<button class="queue-toggle-btn" :class="{ active: $store.queue.visible }" @click="$store.queue.visible = !$store.queue.visible" title="{{ t.player_queue }}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="device-picker" @click.outside="$store.devices.open = false">
|
||||||
|
<button class="queue-toggle-btn device-toggle-btn"
|
||||||
|
:class="{ active: $store.devices.isActive() }"
|
||||||
|
@click="$store.devices.toggle()"
|
||||||
|
:title="$store.devices.activeLabel()"
|
||||||
|
aria-label="Devices">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="12" rx="2"/>
|
||||||
|
<path d="M8 20h8"/>
|
||||||
|
<path d="M12 16v4"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="device-popover" x-show="$store.devices.open" x-transition x-cloak>
|
||||||
|
<template x-for="device in $store.devices.devices" :key="device.id">
|
||||||
|
<button class="device-row"
|
||||||
|
:class="{ active: device.is_active, 'current-device': device.is_current }"
|
||||||
|
@click="$store.devices.select(device.id)">
|
||||||
|
<span class="device-row-icon">
|
||||||
|
<template x-if="device.kind === 'phone'">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="7" y="2" width="10" height="20" rx="2"/>
|
||||||
|
<path d="M11 18h2"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template x-if="device.kind !== 'phone'">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="12" rx="2"/>
|
||||||
|
<path d="M8 20h8"/>
|
||||||
|
<path d="M12 16v4"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span class="device-row-main">
|
||||||
|
<span class="device-row-name" x-text="device.name"></span>
|
||||||
|
<span class="device-row-current" x-show="device.is_current">This device</span>
|
||||||
|
</span>
|
||||||
|
<span class="device-row-check" x-show="device.is_active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="$store.devices.jams.length > 0">
|
||||||
|
<div class="device-section-label jam-section-label">Jams</div>
|
||||||
|
</template>
|
||||||
|
<template x-for="jam in $store.devices.jams" :key="jam.id">
|
||||||
|
<button class="device-row jam-row"
|
||||||
|
:class="{ active: jam.is_active, pending: jam.is_pending }"
|
||||||
|
@click="$store.devices.selectJam(jam)">
|
||||||
|
<span class="device-row-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="8" cy="8" r="3"/>
|
||||||
|
<circle cx="16" cy="8" r="3"/>
|
||||||
|
<path d="M4 20v-1a4 4 0 014-4h0a4 4 0 014 4v1"/>
|
||||||
|
<path d="M12 20v-1a4 4 0 014-4h0a4 4 0 014 4v1"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="device-row-main">
|
||||||
|
<span class="device-row-name" x-text="jam.name"></span>
|
||||||
|
<span class="device-row-current"
|
||||||
|
x-text="!jam.host_device_online ? 'Host offline' : (jam.is_pending ? 'Invite pending' : (jam.is_owner ? 'Your Jam' : 'Shared queue'))"></span>
|
||||||
|
</span>
|
||||||
|
<span class="device-row-check" x-show="jam.is_active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button class="device-row start-jam-row" @click="$store.devices.openJamPanel()">
|
||||||
|
<span class="device-row-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 5v14"/>
|
||||||
|
<path d="M5 12h14"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="device-row-main">
|
||||||
|
<span class="device-row-name">Start Jam</span>
|
||||||
|
<span class="device-row-current">Invite listeners</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="jam-create-panel" x-show="$store.devices.jamPanelOpen" x-transition x-cloak>
|
||||||
|
<div class="jam-selected-users" x-show="$store.devices.jamSelectedUsers.length > 0">
|
||||||
|
<template x-for="user in $store.devices.jamSelectedUsers" :key="user.id">
|
||||||
|
<button class="jam-user-chip" @click="$store.devices.removeJamInvitee(user.id)">
|
||||||
|
<span x-text="user.display_name || user.username"></span>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<input class="jam-user-search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search users"
|
||||||
|
x-model="$store.devices.jamQuery"
|
||||||
|
@input="$store.devices.queueJamSearch()">
|
||||||
|
<div class="jam-search-results" x-show="$store.devices.jamUsers.length > 0">
|
||||||
|
<template x-for="user in $store.devices.jamUsers" :key="user.id">
|
||||||
|
<button class="jam-search-row" @click="$store.devices.addJamInvitee(user)">
|
||||||
|
<span x-text="user.display_name || user.username"></span>
|
||||||
|
<small x-text="user.email || user.username"></small>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button class="jam-create-btn" @click="$store.devices.createJam()">Create Jam</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+927
-36
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user