Music Player & Playlist Manager
This commit is contained in:
@@ -497,6 +497,13 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Playlist track count in selector */
|
||||
.playlist-track-count {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Custom confirm dialog */
|
||||
.confirm-dialog {
|
||||
position: fixed;
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
}
|
||||
|
||||
.nav-item i,
|
||||
.nav-item .nav-icon,
|
||||
.nav-item .oxi-icon {
|
||||
margin-right: 14px;
|
||||
width: 20px;
|
||||
|
||||
@@ -0,0 +1,896 @@
|
||||
/* Music library view */
|
||||
.music-container {
|
||||
display: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.music-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.music-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.music-content {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.music-sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--border-color, #e2e8f0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary, #f8fafc);
|
||||
}
|
||||
|
||||
.music-sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
}
|
||||
|
||||
.music-sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #64748b);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.music-playlist-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.music-playlist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.music-playlist-item:hover {
|
||||
background: var(--hover-bg, #e2e8f0);
|
||||
}
|
||||
|
||||
.music-playlist-item.active {
|
||||
background: var(--active-bg, #dbeafe);
|
||||
}
|
||||
|
||||
.music-playlist-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.music-playlist-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.music-playlist-item-name {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1e293b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.music-playlist-item-count {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.music-main {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.music-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.music-welcome i {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-muted, #cbd5e1);
|
||||
}
|
||||
|
||||
.music-welcome h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.music-welcome p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.music-playlist-detail {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.music-playlist-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.music-playlist-cover {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 48px;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.music-playlist-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.music-playlist-info h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.music-playlist-info p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.music-playlist-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.music-track-list {
|
||||
background: var(--bg-secondary, #f8fafc);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.music-track-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #64748b);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.music-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.music-track:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.music-track:hover {
|
||||
background: var(--hover-bg, #f1f5f9);
|
||||
}
|
||||
|
||||
.music-track-col {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.music-track-num {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.music-track-title {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.music-track-icon {
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.music-track-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.music-track-artist {
|
||||
flex: 1;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.music-track-album {
|
||||
flex: 1;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.music-track-duration {
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.music-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.music-empty i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-muted, #cbd5e1);
|
||||
}
|
||||
|
||||
.music-empty p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.music-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.music-loading i {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.music-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--error-color, #ef4444);
|
||||
}
|
||||
|
||||
.music-error i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.music-error p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
[data-theme="dark"] .music-sidebar {
|
||||
background: var(--bg-secondary, #1e293b);
|
||||
border-color: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .music-playlist-item-name {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .music-welcome h3 {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .music-playlist-info h2 {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .music-track-name {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .music-track-list {
|
||||
background: var(--bg-secondary, #1e293b);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.music-content {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.music-sidebar {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.music-playlist-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.music-playlist-cover {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.music-playlist-info h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.music-track-album {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
MUSIC PLAYER
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.music-player {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 90px;
|
||||
background: var(--bg-primary, #fff);
|
||||
border-top: 1px solid var(--border-color, #e2e8f0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.music-player.has-track {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.music-player.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Track Info */
|
||||
.player-track-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.player-album-art {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.player-track-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.player-track-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1e293b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.player-track-artist {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* Player Controls */
|
||||
.player-controls {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.player-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.player-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-btn:hover {
|
||||
color: var(--text-primary, #1e293b);
|
||||
background: var(--hover-bg, #f1f5f9);
|
||||
}
|
||||
|
||||
.player-btn.active {
|
||||
color: var(--primary-color, #667eea);
|
||||
}
|
||||
|
||||
.player-btn-main {
|
||||
background: var(--primary-color, #667eea);
|
||||
color: #fff !important;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.player-btn-main:hover {
|
||||
background: var(--primary-color-dark, #5a67d8);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.player-btn-small {
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.player-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.player-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.player-time-current {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.player-time-total {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.player-progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--border-color, #e2e8f0);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.player-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-color, #667eea);
|
||||
border-radius: 2px;
|
||||
transition: width 0.1s linear;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.player-progress-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--primary-color, #667eea);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.player-progress-bar:hover .player-progress-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player-progress-bar:hover .player-progress-fill {
|
||||
height: 6px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Volume Controls */
|
||||
.player-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 140px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.player-volume-slider {
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#player-volume-input {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--border-color, #e2e8f0);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-volume-input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--primary-color, #667eea);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-volume-input::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--primary-color, #667eea);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Queue Panel */
|
||||
.player-queue {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
right: 16px;
|
||||
width: 320px;
|
||||
max-height: 400px;
|
||||
background: var(--bg-primary, #fff);
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-queue.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-queue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
}
|
||||
|
||||
.player-queue-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.player-queue-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.player-queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.player-queue-item:hover {
|
||||
background: var(--hover-bg, #f1f5f9);
|
||||
}
|
||||
|
||||
.player-queue-item.active {
|
||||
background: var(--active-bg, #dbeafe);
|
||||
}
|
||||
|
||||
.queue-item-num {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.player-queue-item.active .queue-item-num {
|
||||
color: var(--primary-color, #667eea);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.queue-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.queue-item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1e293b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.queue-item-artist {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.queue-item-duration {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.queue-item-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary, #64748b);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.player-queue-item:hover .queue-item-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.queue-item-remove:hover {
|
||||
color: var(--error-color, #ef4444);
|
||||
}
|
||||
|
||||
.player-queue-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.player-queue-empty i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-muted, #cbd5e1);
|
||||
}
|
||||
|
||||
/* Track playing indicator */
|
||||
.music-track.playing {
|
||||
background: var(--active-bg, #dbeafe);
|
||||
}
|
||||
|
||||
.music-track.playing .music-track-name {
|
||||
color: var(--primary-color, #667eea);
|
||||
}
|
||||
|
||||
.track-num-text.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.track-play-icon {
|
||||
display: none;
|
||||
color: var(--primary-color, #667eea);
|
||||
}
|
||||
|
||||
/* Repeat one icon */
|
||||
.player-btn.repeat-one i::after {
|
||||
content: '1';
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Dark theme for player */
|
||||
[data-theme="dark"] .music-player {
|
||||
background: var(--bg-secondary, #1e293b);
|
||||
border-color: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .player-track-name {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .player-queue {
|
||||
background: var(--bg-secondary, #1e293b);
|
||||
border-color: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .player-queue-header {
|
||||
border-color: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .player-queue-header h3 {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .queue-item-name {
|
||||
color: var(--text-primary, #f1f5f9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] #player-volume-input {
|
||||
background: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .player-progress-bar {
|
||||
background: var(--border-color, #334155);
|
||||
}
|
||||
|
||||
/* Mobile player adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.music-player {
|
||||
height: 70px;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.player-track-info {
|
||||
min-width: auto;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.player-album-art {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.player-track-name {
|
||||
max-width: 70px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.player-track-artist {
|
||||
max-width: 70px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.player-controls {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.player-buttons {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.player-btn {
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.player-btn-main {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.player-extra {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.player-volume-slider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-queue {
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
<link rel="stylesheet" href="/css/views/trash.css">
|
||||
<link rel="stylesheet" href="/css/views/photos.css">
|
||||
<link rel="stylesheet" href="/css/views/photosLightbox.css">
|
||||
<link rel="stylesheet" href="/css/views/music.css">
|
||||
|
||||
<!-- Scripts (defer: download in parallel, execute in order, after HTML parsed) -->
|
||||
<script defer src="/js/core/i18n.js"></script>
|
||||
@@ -36,6 +37,7 @@
|
||||
<script defer src="/js/features/library/recent.js"></script>
|
||||
<script defer src="/js/features/library/photos.js"></script>
|
||||
<script defer src="/js/features/library/photosLightbox.js"></script>
|
||||
<script defer src="/js/features/library/music.js"></script>
|
||||
<script defer src="/js/features/sharing/fileSharing.js"></script>
|
||||
<script defer src="/js/views/shared/sharedView.js"></script>
|
||||
<script defer src="/js/features/files/inlineViewer.js"></script>
|
||||
@@ -89,6 +91,14 @@
|
||||
<i class="fas fa-images"></i>
|
||||
<span data-i18n="nav.photos">Photos</span>
|
||||
</div>
|
||||
<div class="nav-item" id="nav-music">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18V5l12-2v13"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<circle cx="18" cy="16" r="3"/>
|
||||
</svg>
|
||||
<span data-i18n="nav.music">Music</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<i class="fas fa-trash"></i>
|
||||
<span data-i18n="nav.trash">Trash</span>
|
||||
|
||||
@@ -618,7 +618,11 @@ function setupEventListeners() {
|
||||
switchToPhotosSection();
|
||||
break;
|
||||
|
||||
case 'nav.trash':
|
||||
case 'nav.music':
|
||||
switchToMusicSection();
|
||||
break;
|
||||
|
||||
case 'nav.trash':
|
||||
switchToTrashSection();
|
||||
break;
|
||||
|
||||
|
||||
@@ -108,7 +108,8 @@ const VIEW_FLAGS = {
|
||||
recent: 'isRecentView',
|
||||
favorites: 'isFavoritesView',
|
||||
trash: 'isTrashView',
|
||||
photos: 'isPhotosView'
|
||||
photos: 'isPhotosView',
|
||||
music: 'isMusicView'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -158,6 +159,11 @@ function setCurrentSection(section) {
|
||||
window.photosView.hide();
|
||||
}
|
||||
|
||||
// Hide musicView when switching to any other section
|
||||
if (section !== 'music' && window.musicView) {
|
||||
window.musicView.hide();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -323,10 +329,38 @@ function switchToTrashSection() {
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
}
|
||||
|
||||
function switchToMusicSection() {
|
||||
if (!setCurrentSection('music')) return;
|
||||
|
||||
// Hide breadcrumb
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
|
||||
// Hide file containers
|
||||
toggleFileContainer(false);
|
||||
|
||||
// Hide actions-bar
|
||||
window.setActionsBarMode('hidden');
|
||||
|
||||
// Reset files view + remove any error
|
||||
window.ui.resetFilesList();
|
||||
|
||||
// Hide list header (created by resetFilesList)
|
||||
const listHeader = document.querySelector('.list-header');
|
||||
listHeader?.classList.add('hidden');
|
||||
|
||||
// Show music view
|
||||
if (window.musicView) {
|
||||
window.musicView.show();
|
||||
}
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
}
|
||||
|
||||
window.switchToFilesSection = switchToFilesSection;
|
||||
window.switchToSharedSection = switchToSharedSection;
|
||||
window.switchToFavoritesSection = switchToFavoritesSection;
|
||||
window.switchToRecentFilesSection = switchToRecentFilesSection;
|
||||
window.switchToPhotosSection = switchToPhotosSection;
|
||||
window.switchToTrashSection = switchToTrashSection;
|
||||
window.switchToMusicSection = switchToMusicSection;
|
||||
window.syncViewContainers = syncViewContainers;
|
||||
|
||||
+39
-1
@@ -69,6 +69,9 @@ const ui = {
|
||||
<div class="context-menu-item" id="share-file-option">
|
||||
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
|
||||
</div>
|
||||
<div class="context-menu-item hidden" id="add-to-playlist-option">
|
||||
<i class="fas fa-compact-disc"></i> <span data-i18n="music.add_to_playlist">Add to Playlist</span>
|
||||
</div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item" id="rename-file-option">
|
||||
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
|
||||
@@ -271,6 +274,33 @@ const ui = {
|
||||
});
|
||||
}
|
||||
|
||||
// Playlist selection dialog
|
||||
if (!document.getElementById('playlist-dialog')) {
|
||||
const playlistDialog = document.createElement('div');
|
||||
playlistDialog.className = 'share-dialog';
|
||||
playlistDialog.id = 'playlist-dialog';
|
||||
playlistDialog.innerHTML = `
|
||||
<div class="share-dialog-content">
|
||||
<div class="share-dialog-header">
|
||||
<i class="fas fa-music dialog-header-icon"></i>
|
||||
<span data-i18n="music.add_to_playlist">Add to Playlist</span>
|
||||
</div>
|
||||
<div id="playlist-dialog-files-info" class="shared-item-info"></div>
|
||||
<div id="playlist-select-container" class="folder-select-container">
|
||||
</div>
|
||||
<div class="rename-dialog-buttons">
|
||||
<button class="btn btn-secondary" id="playlist-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||
<button class="btn btn-primary" id="playlist-add-btn" data-i18n="music.add">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(playlistDialog);
|
||||
|
||||
document.getElementById('playlist-cancel-btn').addEventListener('click', () => {
|
||||
if (window.contextMenus) window.contextMenus.closePlaylistDialog();
|
||||
});
|
||||
}
|
||||
|
||||
// Assign events to menu items
|
||||
if (window.contextMenus) {
|
||||
window.contextMenus.assignMenuEvents();
|
||||
@@ -855,10 +885,12 @@ const ui = {
|
||||
parent_id: card.dataset.parentId || ''
|
||||
};
|
||||
} else {
|
||||
const fileData = info.data || self._items.get(info.id);
|
||||
window.app.contextMenuTargetFile = {
|
||||
id: info.id,
|
||||
name: card.dataset.fileName,
|
||||
folder_id: card.dataset.folderId || ''
|
||||
folder_id: card.dataset.folderId || '',
|
||||
mime_type: fileData?.mime_type || null
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -936,6 +968,9 @@ const ui = {
|
||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||
}
|
||||
if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') {
|
||||
window.contextMenus.syncAddToPlaylistOption();
|
||||
}
|
||||
menu.style.left = `${e.pageX}px`;
|
||||
menu.style.top = `${e.pageY}px`;
|
||||
menu.style.display = 'block';
|
||||
@@ -1406,6 +1441,9 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||
}
|
||||
if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') {
|
||||
window.contextMenus.syncAddToPlaylistOption();
|
||||
}
|
||||
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
|
||||
@@ -253,6 +253,34 @@ const _ICONS = {
|
||||
384,
|
||||
'M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z'
|
||||
],
|
||||
pause: [
|
||||
384,
|
||||
'M48 32C21.5 32 0 53.5 0 80L0 432c0 26.5 21.5 48 48 48l64 0c26.5 0 48-21.5 48-48l0-352c0-26.5-21.5-48-48-48L48 32zm224 0c-26.5 0-48 21.5-48 48l0 352c0 26.5 21.5 48 48 48l64 0c26.5 0 48-21.5 48-48l0-352c0-26.5-21.5-48-48-48l-64 0z'
|
||||
],
|
||||
shuffle: [
|
||||
512,
|
||||
'M403.8 34.4c12-5 25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64c-9.2 9.2-22.9 11.9-34.9 6.9S384 204.9 384 192l0-32-32 0c-10.1 0-19.6 4.7-25.6 12.8l-32.4 43.2-40-53.3 21.2-28.3C293.3 110.2 321.8 96 352 96l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6zM154 296l40 53.3-21.2 28.3C154.7 401.8 126.2 416 96 416l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c10.1 0 19.6-4.7 25.6-12.8L154 296zM438.6 470.6c-9.2 9.2-22.9 11.9-34.9 6.9S384 460.9 384 448l0-32-32 0c-30.2 0-58.7-14.2-76.8-38.4L121.6 172.8c-6-8.1-15.5-12.8-25.6-12.8l-64 0c-17.7 0-32-14.3-32-32S14.3 96 32 96l64 0c30.2 0 58.7 14.2 76.8 38.4L326.4 339.2c6 8.1 15.5 12.8 25.6 12.8l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6s25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64z'
|
||||
],
|
||||
repeat: [
|
||||
512,
|
||||
'M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z'
|
||||
],
|
||||
forward: [
|
||||
512,
|
||||
'M371.7 43.1C360.1 32 343 28.9 328.3 35.2S304 56 304 72l0 136.3-172.3-165.1C120.1 32 103 28.9 88.3 35.2S64 56 64 72l0 368c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9L304 303.7 304 440c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9l192-184c7.9-7.5 12.3-18 12.3-28.9s-4.5-21.3-12.3-28.9l-192-184z'
|
||||
],
|
||||
backward: [
|
||||
512,
|
||||
'M204.3 43.1C215.9 32 233 28.9 247.7 35.2S272 56 272 72l0 136.3 172.3-165.1C455.9 32 473 28.9 487.7 35.2S512 56 512 72l0 368c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9L272 303.7 272 440c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9l-192-184C4.5 277.3 0 266.9 0 256s4.5-21.3 12.3-28.9l192-184z'
|
||||
],
|
||||
volume: [
|
||||
512,
|
||||
'M48 352l48 0 134.1 119.2c6.4 5.7 14.6 8.8 23.1 8.8 19.2 0 34.8-15.6 34.8-34.8l0-378.4c0-19.2-15.6-34.8-34.8-34.8-8.5 0-16.7 3.1-23.1 8.8L96 160 48 160c-26.5 0-48 21.5-48 48l0 96c0 26.5 21.5 48 48 48zM441.1 107c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C443.3 170.7 464 210.9 464 256s-20.7 85.3-53.2 111.8c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5c43.2-35.2 70.9-88.9 70.9-149s-27.7-113.8-70.9-149zm-60.5 74.5c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C361.1 227.6 368 241 368 256s-6.9 28.4-17.7 37.3c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5C402.1 312.9 416 286.1 416 256s-13.9-56.9-35.5-74.5z'
|
||||
],
|
||||
'compact-disc': [
|
||||
512,
|
||||
'M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zM88 256H56c0-105.9 86.1-192 192-192v32c-88.2 0-160 71.8-160 160zm160 96c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm0-128c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z'
|
||||
],
|
||||
'question-circle': [
|
||||
512,
|
||||
'M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z'
|
||||
|
||||
@@ -51,6 +51,19 @@ const contextMenus = {
|
||||
}
|
||||
},
|
||||
|
||||
syncAddToPlaylistOption() {
|
||||
const option = document.getElementById('add-to-playlist-option');
|
||||
if (!option) return;
|
||||
|
||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
||||
if (targetFile) {
|
||||
const isAudio = targetFile.mime_type && targetFile.mime_type.startsWith('audio/');
|
||||
option.classList.toggle('hidden', !isAudio);
|
||||
} else {
|
||||
option.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign events to menu items and dialogs
|
||||
*/
|
||||
@@ -217,6 +230,18 @@ const contextMenus = {
|
||||
window.ui.closeFileContextMenu();
|
||||
});
|
||||
|
||||
document.getElementById('add-to-playlist-option').addEventListener('click', () => {
|
||||
const file = window.app.contextMenuTargetFile;
|
||||
if (file) {
|
||||
this.showPlaylistDialog(file);
|
||||
}
|
||||
window.ui.closeFileContextMenu();
|
||||
});
|
||||
|
||||
document.getElementById('playlist-add-btn').addEventListener('click', () => {
|
||||
this.addSelectedFilesToPlaylist();
|
||||
});
|
||||
|
||||
document.getElementById('delete-file-option').addEventListener('click', async () => {
|
||||
const file = window.app.contextMenuTargetFile;
|
||||
window.ui.closeFileContextMenu();
|
||||
@@ -1049,6 +1074,147 @@ const contextMenus = {
|
||||
closeNotificationDialog() {
|
||||
document.getElementById('notification-dialog').style.display = 'none';
|
||||
window.app.notificationShareUrl = null;
|
||||
},
|
||||
|
||||
_selectedPlaylistId: null,
|
||||
|
||||
async showPlaylistDialog(file) {
|
||||
const dialog = document.getElementById('playlist-dialog');
|
||||
const container = document.getElementById('playlist-select-container');
|
||||
const filesInfo = document.getElementById('playlist-dialog-files-info');
|
||||
|
||||
if (!dialog || !container) {
|
||||
console.error('Playlist dialog elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the file(s) to add
|
||||
window.app.playlistDialogFiles = [file];
|
||||
|
||||
// Update files info
|
||||
if (filesInfo) {
|
||||
filesInfo.innerHTML = `<strong>${window.i18n ? window.i18n.t('music.selected_files', 'Selected:') : 'Selected:'} </strong>${file.name}`;
|
||||
}
|
||||
|
||||
// Reset selection
|
||||
this._selectedPlaylistId = null;
|
||||
container.innerHTML = '<div class="folder-select-loading"><i class="fas fa-spinner fa-spin"></i></div>';
|
||||
|
||||
// Reset add button state
|
||||
const addBtn = document.getElementById('playlist-add-btn');
|
||||
if (addBtn) addBtn.disabled = true;
|
||||
|
||||
// Show dialog
|
||||
dialog.style.display = 'flex';
|
||||
requestAnimationFrame(() => dialog.classList.add('active'));
|
||||
|
||||
// Load playlists
|
||||
try {
|
||||
const resp = await fetch('/api/playlists', { credentials: 'include' });
|
||||
if (!resp.ok) throw new Error('Failed to load playlists');
|
||||
|
||||
const playlists = await resp.json();
|
||||
this._renderPlaylistSelect(container, playlists);
|
||||
} catch (err) {
|
||||
console.error('Error loading playlists:', err);
|
||||
container.innerHTML = `<div class="folder-select-empty">${window.i18n ? window.i18n.t('music.load_error', 'Error loading playlists') : 'Error loading playlists'}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
_renderPlaylistSelect(container, playlists) {
|
||||
const t = (key, fallback) => window.i18n ? window.i18n.t(key, fallback) : fallback;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (playlists.length === 0) {
|
||||
container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
playlists.forEach(playlist => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'folder-select-item';
|
||||
item.dataset.id = playlist.id;
|
||||
item.innerHTML = `
|
||||
<i class="fas fa-list"></i>
|
||||
<span>${this._escapeHtml(playlist.name)}</span>
|
||||
<span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span>
|
||||
`;
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
container.querySelectorAll('.folder-select-item').forEach(el => el.classList.remove('selected'));
|
||||
item.classList.add('selected');
|
||||
this._selectedPlaylistId = playlist.id;
|
||||
const addBtn = document.getElementById('playlist-add-btn');
|
||||
if (addBtn) addBtn.disabled = false;
|
||||
});
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
},
|
||||
|
||||
async addSelectedFilesToPlaylist() {
|
||||
const playlistId = this._selectedPlaylistId;
|
||||
const files = window.app.playlistDialogFiles || [];
|
||||
|
||||
if (!playlistId || files.length === 0) return;
|
||||
|
||||
const addBtn = document.getElementById('playlist-add-btn');
|
||||
if (addBtn) addBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/playlists/${playlistId}/tracks`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getCsrfHeaders()
|
||||
},
|
||||
body: JSON.stringify({ file_ids: files.map(f => f.id) })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.message || 'Failed to add tracks');
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
window.ui.showNotification(
|
||||
window.i18n ? window.i18n.t('music.added', 'Added!') : 'Added!',
|
||||
`${files.length} ${files.length === 1 ? 'track' : 'tracks'} ${window.i18n ? window.i18n.t('music.added_to_playlist', 'added to playlist') : 'added to playlist'}`
|
||||
);
|
||||
|
||||
this.closePlaylistDialog();
|
||||
|
||||
// Refresh music view if open
|
||||
if (window.musicView && window.musicView.playlists) {
|
||||
window.musicView._loadPlaylists();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error adding to playlist:', err);
|
||||
window.ui.showNotification(
|
||||
window.i18n ? window.i18n.t('music.error', 'Error') : 'Error',
|
||||
err.message || window.i18n.t('music.add_error', 'Could not add tracks to playlist')
|
||||
);
|
||||
if (addBtn) addBtn.disabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
closePlaylistDialog() {
|
||||
const dialog = document.getElementById('playlist-dialog');
|
||||
if (dialog) {
|
||||
dialog.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
dialog.style.display = 'none';
|
||||
}, 200);
|
||||
}
|
||||
window.app.playlistDialogFiles = null;
|
||||
this._selectedPlaylistId = null;
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,980 @@
|
||||
/**
|
||||
* OxiCloud - Music Library View
|
||||
* Playlist management with track listings and audio player
|
||||
*/
|
||||
|
||||
const musicView = {
|
||||
playlists: [],
|
||||
currentPlaylist: null,
|
||||
currentTracks: [],
|
||||
loading: false,
|
||||
_container: null,
|
||||
_initialized: false,
|
||||
selected: new Set(),
|
||||
|
||||
_headers(json = false) {
|
||||
const h = typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
||||
if (json) h['Content-Type'] = 'application/json';
|
||||
return h;
|
||||
},
|
||||
|
||||
init() {
|
||||
if (!this._container) {
|
||||
const contentArea = document.querySelector('.content-area');
|
||||
if (!contentArea) return;
|
||||
const el = document.createElement('div');
|
||||
el.id = 'music-container';
|
||||
el.className = 'music-container';
|
||||
contentArea.appendChild(el);
|
||||
this._container = el;
|
||||
}
|
||||
musicPlayer.init();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
show() {
|
||||
this.init();
|
||||
if (!this._container) return;
|
||||
this._container.classList.add('active');
|
||||
this.currentPlaylist = null;
|
||||
this.currentTracks = [];
|
||||
this._container.innerHTML = '';
|
||||
this._renderPlaylists();
|
||||
this._loadPlaylists();
|
||||
},
|
||||
|
||||
hide() {
|
||||
if (this._container) {
|
||||
this._container.classList.remove('active');
|
||||
}
|
||||
this.currentPlaylist = null;
|
||||
this.currentTracks = [];
|
||||
},
|
||||
|
||||
async _loadPlaylists() {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
this._showLoading(true);
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/playlists', {
|
||||
credentials: 'include',
|
||||
headers: this._headers()
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to load playlists');
|
||||
|
||||
this.playlists = await resp.json();
|
||||
this._renderPlaylists();
|
||||
} catch (err) {
|
||||
console.error('Music load error:', err);
|
||||
this._showError(err.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this._showLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
_renderPlaylists() {
|
||||
if (!this._container) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
this._container.innerHTML = `
|
||||
<div class="music-toolbar">
|
||||
<button class="btn btn-primary" id="music-create-playlist-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>${t('music.create_playlist', 'Create Playlist')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="music-content">
|
||||
<div class="music-sidebar">
|
||||
<div class="music-sidebar-header">
|
||||
<h3>${t('music.playlists', 'Playlists')}</h3>
|
||||
</div>
|
||||
<div class="music-playlist-list" id="music-playlist-list">
|
||||
${this.playlists.length === 0 ? `
|
||||
<div class="music-empty">
|
||||
<i class="fas fa-music"></i>
|
||||
<p>${t('music.no_playlists', 'No playlists yet')}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-main">
|
||||
<div class="music-welcome">
|
||||
<i class="fas fa-music"></i>
|
||||
<h3>${t('music.select_playlist', 'Select a playlist')}</h3>
|
||||
<p>${t('music.select_hint', 'Choose a playlist from the sidebar or create a new one')}</p>
|
||||
</div>
|
||||
<div class="music-playlist-detail hidden" id="music-playlist-detail">
|
||||
<div class="music-playlist-header">
|
||||
<div class="music-playlist-cover">
|
||||
<i class="fas fa-music"></i>
|
||||
</div>
|
||||
<div class="music-playlist-info">
|
||||
<h2 id="music-playlist-name"></h2>
|
||||
<p id="music-playlist-meta"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-playlist-actions">
|
||||
<button class="btn btn-secondary" id="music-play-all-btn">
|
||||
<i class="fas fa-play"></i>
|
||||
<span>${t('music.play_all', 'Play All')}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="music-shuffle-btn">
|
||||
<i class="fas fa-shuffle"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="music-add-tracks-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>${t('music.add_tracks', 'Add Tracks')}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="music-edit-playlist-btn">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="music-share-playlist-btn">
|
||||
<i class="fas fa-share"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="music-delete-playlist-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="music-track-list" id="music-track-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this._bindPlaylistEvents();
|
||||
this._renderPlaylistList();
|
||||
},
|
||||
|
||||
_renderPlaylistList() {
|
||||
const listEl = document.getElementById('music-playlist-list');
|
||||
if (!listEl) return;
|
||||
|
||||
if (this.playlists.length === 0) {
|
||||
listEl.innerHTML = `
|
||||
<div class="music-empty">
|
||||
<i class="fas fa-music"></i>
|
||||
<p>No playlists yet</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
listEl.innerHTML = this.playlists.map(p => `
|
||||
<div class="music-playlist-item" data-id="${p.id}">
|
||||
<div class="music-playlist-icon">
|
||||
<i class="fas fa-list"></i>
|
||||
</div>
|
||||
<div class="music-playlist-item-info">
|
||||
<span class="music-playlist-item-name">${this._escapeHtml(p.name)}</span>
|
||||
<span class="music-playlist-item-count">${p.track_count || 0} ${t('music.tracks', 'tracks')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.music-playlist-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.id;
|
||||
this._selectPlaylist(id);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_bindPlaylistEvents() {
|
||||
const createBtn = document.getElementById('music-create-playlist-btn');
|
||||
if (createBtn) {
|
||||
createBtn.addEventListener('click', () => this._showCreatePlaylistDialog());
|
||||
}
|
||||
|
||||
const playAllBtn = document.getElementById('music-play-all-btn');
|
||||
if (playAllBtn) {
|
||||
playAllBtn.addEventListener('click', () => this._playAll());
|
||||
}
|
||||
|
||||
const shuffleBtn = document.getElementById('music-shuffle-btn');
|
||||
if (shuffleBtn) {
|
||||
shuffleBtn.addEventListener('click', () => this._shufflePlay());
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('music-delete-playlist-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', () => this._deletePlaylist());
|
||||
}
|
||||
},
|
||||
|
||||
async _selectPlaylist(playlistId) {
|
||||
const playlist = this.playlists.find(p => p.id === playlistId);
|
||||
if (!playlist) return;
|
||||
|
||||
this.currentPlaylist = playlist;
|
||||
|
||||
const detailEl = document.getElementById('music-playlist-detail');
|
||||
const welcomeEl = this._container.querySelector('.music-welcome');
|
||||
const nameEl = document.getElementById('music-playlist-name');
|
||||
const metaEl = document.getElementById('music-playlist-meta');
|
||||
|
||||
if (welcomeEl) welcomeEl.classList.add('hidden');
|
||||
if (detailEl) detailEl.classList.remove('hidden');
|
||||
if (nameEl) nameEl.textContent = playlist.name;
|
||||
if (metaEl) {
|
||||
metaEl.textContent = `${playlist.track_count || 0} tracks`;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.music-playlist-item').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.id === playlistId);
|
||||
});
|
||||
|
||||
await this._loadPlaylistTracks(playlistId);
|
||||
},
|
||||
|
||||
async _loadPlaylistTracks(playlistId) {
|
||||
const trackListEl = document.getElementById('music-track-list');
|
||||
if (!trackListEl) return;
|
||||
|
||||
trackListEl.innerHTML = '<div class="music-loading"><i class="fas fa-spinner fa-spin"></i></div>';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/playlists/${playlistId}/tracks`, {
|
||||
credentials: 'include',
|
||||
headers: this._headers()
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to load tracks');
|
||||
|
||||
this.currentTracks = await resp.json();
|
||||
this._renderTracks();
|
||||
} catch (err) {
|
||||
console.error('Track load error:', err);
|
||||
trackListEl.innerHTML = `<div class="music-error">${err.message}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
_renderTracks() {
|
||||
const trackListEl = document.getElementById('music-track-list');
|
||||
if (!trackListEl) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (this.currentTracks.length === 0) {
|
||||
trackListEl.innerHTML = `
|
||||
<div class="music-empty">
|
||||
<i class="fas fa-music"></i>
|
||||
<p>${t('music.no_tracks', 'No tracks in this playlist')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
trackListEl.innerHTML = `
|
||||
<div class="music-track-header">
|
||||
<span class="music-track-col music-track-num">#</span>
|
||||
<span class="music-track-col music-track-title">${t('music.title', 'Title')}</span>
|
||||
<span class="music-track-col music-track-artist">${t('music.artist', 'Artist')}</span>
|
||||
<span class="music-track-col music-track-album">${t('music.album', 'Album')}</span>
|
||||
<span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span>
|
||||
</div>
|
||||
${this.currentTracks.map((track, idx) => `
|
||||
<div class="music-track ${musicPlayer.currentTrack?.id === track.id ? 'playing' : ''}" data-idx="${idx}" data-id="${track.id}" data-file-id="${track.file_id}">
|
||||
<span class="music-track-col music-track-num">
|
||||
<span class="track-num-text">${idx + 1}</span>
|
||||
<i class="fas fa-play track-play-icon hidden"></i>
|
||||
</span>
|
||||
<span class="music-track-col music-track-title">
|
||||
<i class="fas fa-music music-track-icon"></i>
|
||||
<span class="music-track-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
|
||||
</span>
|
||||
<span class="music-track-col music-track-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
|
||||
<span class="music-track-col music-track-album">${this._escapeHtml(track.album || '-')}</span>
|
||||
<span class="music-track-col music-track-duration">${this._formatDuration(track.duration_secs)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
const self = this;
|
||||
trackListEl.querySelectorAll('.music-track').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const idx = parseInt(row.dataset.idx);
|
||||
self._playTrack(idx);
|
||||
});
|
||||
|
||||
row.addEventListener('dblclick', () => {
|
||||
const idx = parseInt(row.dataset.idx);
|
||||
self._playTrack(idx);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_playTrack(idx) {
|
||||
if (!this.currentTracks[idx]) return;
|
||||
|
||||
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || 'Playlist');
|
||||
musicPlayer.playTrack(idx);
|
||||
},
|
||||
|
||||
_playAll() {
|
||||
if (this.currentTracks.length > 0) {
|
||||
this._playTrack(0);
|
||||
}
|
||||
},
|
||||
|
||||
_shufflePlay() {
|
||||
if (this.currentTracks.length > 0) {
|
||||
const shuffled = [...this.currentTracks].sort(() => Math.random() - 0.5);
|
||||
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || 'Shuffle');
|
||||
musicPlayer.playTrack(0);
|
||||
}
|
||||
},
|
||||
|
||||
_showCreatePlaylistDialog() {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
const name = prompt(t('music.playlist_name', 'Playlist name:'));
|
||||
if (!name || !name.trim()) return;
|
||||
|
||||
this._createPlaylist(name.trim());
|
||||
},
|
||||
|
||||
async _createPlaylist(name) {
|
||||
try {
|
||||
const resp = await fetch('/api/playlists', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: this._headers(true),
|
||||
body: JSON.stringify({ name, description: null })
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to create playlist');
|
||||
|
||||
const playlist = await resp.json();
|
||||
this.playlists.unshift(playlist);
|
||||
this._renderPlaylistList();
|
||||
this._selectPlaylist(playlist.id);
|
||||
} catch (err) {
|
||||
console.error('Create playlist error:', err);
|
||||
alert('Failed to create playlist: ' + err.message);
|
||||
}
|
||||
},
|
||||
|
||||
async _deletePlaylist() {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (!this.currentPlaylist) return;
|
||||
if (!confirm(t('music.confirm_delete', 'Delete this playlist?'))) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: this._headers()
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to delete playlist');
|
||||
|
||||
this.playlists = this.playlists.filter(p => p.id !== this.currentPlaylist.id);
|
||||
this.currentPlaylist = null;
|
||||
this.currentTracks = [];
|
||||
this._renderPlaylists();
|
||||
} catch (err) {
|
||||
console.error('Delete playlist error:', err);
|
||||
alert('Failed to delete playlist: ' + err.message);
|
||||
}
|
||||
},
|
||||
|
||||
_formatDuration(secs) {
|
||||
if (!secs) return '-';
|
||||
const mins = Math.floor(secs / 60);
|
||||
const s = Math.floor(secs % 60);
|
||||
return `${mins}:${s.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
},
|
||||
|
||||
_showLoading(show) {
|
||||
const existing = this._container?.querySelector('.music-loading');
|
||||
if (show && !existing) {
|
||||
const loading = document.createElement('div');
|
||||
loading.className = 'music-loading';
|
||||
loading.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
this._container?.appendChild(loading);
|
||||
} else if (!show && existing) {
|
||||
existing.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_showError(message) {
|
||||
if (!this._container) return;
|
||||
this._container.innerHTML = `
|
||||
<div class="music-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>${this._escapeHtml(message)}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Music Player Module
|
||||
* Handles audio playback, queue, and controls
|
||||
*/
|
||||
const musicPlayer = {
|
||||
audio: null,
|
||||
queue: [],
|
||||
currentIndex: -1,
|
||||
currentTrack: null,
|
||||
isPlaying: false,
|
||||
volume: 0.7,
|
||||
isMuted: false,
|
||||
shuffle: false,
|
||||
repeat: 'none',
|
||||
playlistName: '',
|
||||
_initialized: false,
|
||||
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
|
||||
this.audio = new Audio();
|
||||
this.audio.volume = this.volume;
|
||||
|
||||
this.audio.addEventListener('ended', () => this._onEnded());
|
||||
this.audio.addEventListener('timeupdate', () => this._onTimeUpdate());
|
||||
this.audio.addEventListener('loadedmetadata', () => this._onLoadedMetadata());
|
||||
this.audio.addEventListener('play', () => this._onPlay());
|
||||
this.audio.addEventListener('pause', () => this._onPause());
|
||||
this.audio.addEventListener('error', (e) => this._onError(e));
|
||||
this.audio.addEventListener('canplay', () => this._onCanPlay());
|
||||
|
||||
this._createPlayerUI();
|
||||
this._bindEvents();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
_createPlayerUI() {
|
||||
if (document.getElementById('music-player')) return;
|
||||
|
||||
const playerEl = document.createElement('div');
|
||||
playerEl.id = 'music-player';
|
||||
playerEl.className = 'music-player';
|
||||
playerEl.innerHTML = `
|
||||
<div class="player-track-info">
|
||||
<div class="player-album-art">
|
||||
<i class="fas fa-music"></i>
|
||||
</div>
|
||||
<div class="player-track-details">
|
||||
<span class="player-track-name">${i18n?.t('music.not_playing', 'Not playing') || 'Not playing'}</span>
|
||||
<span class="player-track-artist"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls">
|
||||
<div class="player-buttons">
|
||||
<button class="player-btn" id="player-shuffle-btn" title="${i18n?.t('music.shuffle', 'Shuffle') || 'Shuffle'}">
|
||||
<i class="fas fa-shuffle"></i>
|
||||
</button>
|
||||
<button class="player-btn" id="player-prev-btn" title="${i18n?.t('music.previous', 'Previous') || 'Previous'}">
|
||||
<i class="fas fa-backward"></i>
|
||||
</button>
|
||||
<button class="player-btn player-btn-main" id="player-play-btn" title="${i18n?.t('music.play', 'Play') || 'Play'}">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<button class="player-btn" id="player-next-btn" title="${i18n?.t('music.next', 'Next') || 'Next'}">
|
||||
<i class="fas fa-forward"></i>
|
||||
</button>
|
||||
<button class="player-btn" id="player-repeat-btn" title="${i18n?.t('music.repeat', 'Repeat') || 'Repeat'}">
|
||||
<i class="fas fa-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-progress">
|
||||
<span class="player-time player-time-current" id="player-current-time">0:00</span>
|
||||
<div class="player-progress-bar" id="player-progress-bar">
|
||||
<div class="player-progress-fill" id="player-progress-fill"></div>
|
||||
<div class="player-progress-handle" id="player-progress-handle"></div>
|
||||
</div>
|
||||
<span class="player-time player-time-total" id="player-total-time">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-extra">
|
||||
<button class="player-btn player-btn-small" id="player-playlist-btn" title="${i18n?.t('music.queue', 'Queue') || 'Queue'}">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
<button class="player-btn player-btn-small" id="player-vol-btn" title="${i18n?.t('music.volume', 'Volume') || 'Volume'}">
|
||||
<i class="fas fa-volume"></i>
|
||||
</button>
|
||||
<div class="player-volume-slider" id="player-volume-slider">
|
||||
<input type="range" min="0" max="100" value="70" id="player-volume-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-queue hidden" id="player-queue">
|
||||
<div class="player-queue-header">
|
||||
<h3>${i18n?.t('music.queue', 'Queue') || 'Queue'}</h3>
|
||||
<button class="player-btn player-btn-small" id="player-close-queue-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-queue-list" id="player-queue-list"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(playerEl);
|
||||
},
|
||||
|
||||
_bindEvents() {
|
||||
const playBtn = document.getElementById('player-play-btn');
|
||||
const prevBtn = document.getElementById('player-prev-btn');
|
||||
const nextBtn = document.getElementById('player-next-btn');
|
||||
const shuffleBtn = document.getElementById('player-shuffle-btn');
|
||||
const repeatBtn = document.getElementById('player-repeat-btn');
|
||||
const progressBar = document.getElementById('player-progress-bar');
|
||||
const volumeInput = document.getElementById('player-volume-input');
|
||||
const volBtn = document.getElementById('player-vol-btn');
|
||||
const playlistBtn = document.getElementById('player-playlist-btn');
|
||||
const closeQueueBtn = document.getElementById('player-close-queue-btn');
|
||||
const queue = document.getElementById('player-queue');
|
||||
|
||||
if (playBtn) {
|
||||
playBtn.addEventListener('click', () => this.togglePlay());
|
||||
}
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', () => this.prev());
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', () => this.next());
|
||||
}
|
||||
|
||||
if (shuffleBtn) {
|
||||
shuffleBtn.addEventListener('click', () => this.toggleShuffle());
|
||||
}
|
||||
|
||||
if (repeatBtn) {
|
||||
repeatBtn.addEventListener('click', () => this.toggleRepeat());
|
||||
}
|
||||
|
||||
if (progressBar) {
|
||||
progressBar.addEventListener('click', (e) => this._seek(e));
|
||||
}
|
||||
|
||||
if (volumeInput) {
|
||||
volumeInput.addEventListener('input', (e) => {
|
||||
this.setVolume(e.target.value / 100);
|
||||
});
|
||||
}
|
||||
|
||||
if (volBtn) {
|
||||
volBtn.addEventListener('click', () => this.toggleMute());
|
||||
}
|
||||
|
||||
if (playlistBtn) {
|
||||
playlistBtn.addEventListener('click', () => this._toggleQueue());
|
||||
}
|
||||
|
||||
if (closeQueueBtn) {
|
||||
closeQueueBtn.addEventListener('click', () => this._toggleQueue(false));
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
queue.addEventListener('click', (e) => {
|
||||
if (e.target === queue) {
|
||||
this._toggleQueue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setQueue(tracks, playlistName = '') {
|
||||
this.queue = [...tracks];
|
||||
this.playlistName = playlistName;
|
||||
this._updateQueueUI();
|
||||
},
|
||||
|
||||
playTrack(index) {
|
||||
if (index < 0 || index >= this.queue.length) return;
|
||||
|
||||
this.currentIndex = index;
|
||||
this.currentTrack = this.queue[index];
|
||||
this._loadAndPlay();
|
||||
},
|
||||
|
||||
_loadAndPlay() {
|
||||
if (!this.currentTrack) return;
|
||||
|
||||
const fileId = this.currentTrack.file_id;
|
||||
this.audio.src = `/api/files/${fileId}`;
|
||||
this.audio.play().catch(err => {
|
||||
console.error('Playback error:', err);
|
||||
});
|
||||
this.isPlaying = true;
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
togglePlay() {
|
||||
if (!this.currentTrack) {
|
||||
if (this.queue.length > 0) {
|
||||
this.playTrack(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.audio.pause();
|
||||
} else {
|
||||
this.audio.play();
|
||||
}
|
||||
},
|
||||
|
||||
play() {
|
||||
if (this.currentTrack && !this.isPlaying) {
|
||||
this.audio.play();
|
||||
}
|
||||
},
|
||||
|
||||
pause() {
|
||||
if (this.isPlaying) {
|
||||
this.audio.pause();
|
||||
}
|
||||
},
|
||||
|
||||
next() {
|
||||
if (this.queue.length === 0) return;
|
||||
|
||||
let nextIndex;
|
||||
if (this.shuffle) {
|
||||
nextIndex = Math.floor(Math.random() * this.queue.length);
|
||||
} else {
|
||||
nextIndex = this.currentIndex + 1;
|
||||
if (nextIndex >= this.queue.length) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
}
|
||||
this.playTrack(nextIndex);
|
||||
},
|
||||
|
||||
prev() {
|
||||
if (this.queue.length === 0) return;
|
||||
|
||||
if (this.audio.currentTime > 3) {
|
||||
this.audio.currentTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let prevIndex = this.currentIndex - 1;
|
||||
if (prevIndex < 0) {
|
||||
prevIndex = this.queue.length - 1;
|
||||
}
|
||||
this.playTrack(prevIndex);
|
||||
},
|
||||
|
||||
toggleShuffle() {
|
||||
this.shuffle = !this.shuffle;
|
||||
const btn = document.getElementById('player-shuffle-btn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', this.shuffle);
|
||||
}
|
||||
},
|
||||
|
||||
toggleRepeat() {
|
||||
const modes = ['none', 'all', 'one'];
|
||||
const currentIdx = modes.indexOf(this.repeat);
|
||||
this.repeat = modes[(currentIdx + 1) % modes.length];
|
||||
|
||||
const btn = document.getElementById('player-repeat-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('active', 'repeat-one');
|
||||
if (this.repeat === 'all') {
|
||||
btn.classList.add('active');
|
||||
} else if (this.repeat === 'one') {
|
||||
btn.classList.add('active', 'repeat-one');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setVolume(vol) {
|
||||
this.volume = Math.max(0, Math.min(1, vol));
|
||||
this.audio.volume = this.volume;
|
||||
this.isMuted = this.volume === 0;
|
||||
this._updateVolumeIcon();
|
||||
|
||||
const input = document.getElementById('player-volume-input');
|
||||
if (input) {
|
||||
input.value = this.volume * 100;
|
||||
}
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
this.isMuted = !this.isMuted;
|
||||
this.audio.muted = this.isMuted;
|
||||
this._updateVolumeIcon();
|
||||
},
|
||||
|
||||
_updateVolumeIcon() {
|
||||
const btn = document.getElementById('player-vol-btn');
|
||||
if (!btn) return;
|
||||
|
||||
let icon;
|
||||
if (this.isMuted || this.volume === 0) {
|
||||
icon = 'fa-volume-mute';
|
||||
} else if (this.volume < 0.5) {
|
||||
icon = 'fa-volume-down';
|
||||
} else {
|
||||
icon = 'fa-volume-up';
|
||||
}
|
||||
|
||||
btn.querySelector('i').className = `fas ${icon}`;
|
||||
},
|
||||
|
||||
_seek(e) {
|
||||
const bar = document.getElementById('player-progress-bar');
|
||||
if (!bar) return;
|
||||
|
||||
const rect = bar.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
if (this.audio.duration) {
|
||||
this.audio.currentTime = percent * this.audio.duration;
|
||||
}
|
||||
},
|
||||
|
||||
_onEnded() {
|
||||
if (this.repeat === 'one') {
|
||||
this.audio.currentTime = 0;
|
||||
this.audio.play();
|
||||
} else if (this.repeat === 'all' || this.currentIndex < this.queue.length - 1) {
|
||||
this.next();
|
||||
} else {
|
||||
this.isPlaying = false;
|
||||
this._updateUI();
|
||||
}
|
||||
},
|
||||
|
||||
_onTimeUpdate() {
|
||||
if (!this.audio.duration) return;
|
||||
|
||||
const current = this.audio.currentTime;
|
||||
const total = this.audio.duration;
|
||||
const percent = (current / total) * 100;
|
||||
|
||||
const fill = document.getElementById('player-progress-fill');
|
||||
const handle = document.getElementById('player-progress-handle');
|
||||
const currentTimeEl = document.getElementById('player-current-time');
|
||||
|
||||
if (fill) fill.style.width = `${percent}%`;
|
||||
if (handle) handle.style.left = `${percent}%`;
|
||||
if (currentTimeEl) currentTimeEl.textContent = this._formatTime(current);
|
||||
},
|
||||
|
||||
_onLoadedMetadata() {
|
||||
const totalTimeEl = document.getElementById('player-total-time');
|
||||
if (totalTimeEl) {
|
||||
totalTimeEl.textContent = this._formatTime(this.audio.duration);
|
||||
}
|
||||
|
||||
if (this.currentTrack && this.audio.duration) {
|
||||
this.currentTrack.duration_secs = Math.round(this.audio.duration);
|
||||
|
||||
const trackDurationEl = document.querySelector(`.music-track[data-idx="${this.currentIndex}"] .music-track-duration`);
|
||||
if (trackDurationEl) {
|
||||
trackDurationEl.textContent = this._formatDuration(this.audio.duration);
|
||||
}
|
||||
|
||||
const queueDurationEl = document.querySelector(`.queue-item[data-idx="${this.currentIndex}"] .queue-item-duration`);
|
||||
if (queueDurationEl) {
|
||||
queueDurationEl.textContent = this._formatDuration(this.audio.duration);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onPlay() {
|
||||
this.isPlaying = true;
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
_onPause() {
|
||||
this.isPlaying = false;
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
_onCanPlay() {
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
_onError(e) {
|
||||
console.error('Audio error:', e);
|
||||
this.isPlaying = false;
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
_updateUI() {
|
||||
const playBtn = document.getElementById('player-play-btn');
|
||||
const trackName = document.querySelector('.player-track-name');
|
||||
const trackArtist = document.querySelector('.player-track-artist');
|
||||
|
||||
if (playBtn) {
|
||||
const icon = playBtn.querySelector('i') || playBtn.querySelector('svg');
|
||||
if (icon) {
|
||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||
const extraClass = 'player-btn-main';
|
||||
if (window.oxiIcon) {
|
||||
icon.outerHTML = window.oxiIcon(iconName, extraClass);
|
||||
} else {
|
||||
icon.className = `fas fa-${iconName} ${extraClass}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trackName) {
|
||||
trackName.textContent = this.currentTrack
|
||||
? (this.currentTrack.title || this.currentTrack.file_name || 'Unknown')
|
||||
: (i18n?.t('music.not_playing', 'Not playing') || 'Not playing');
|
||||
}
|
||||
|
||||
if (trackArtist) {
|
||||
trackArtist.textContent = this.currentTrack?.artist || '';
|
||||
}
|
||||
|
||||
if (musicView.currentTracks.length > 0) {
|
||||
document.querySelectorAll('.music-track').forEach(row => {
|
||||
const idx = parseInt(row.dataset.idx);
|
||||
row.classList.toggle('playing', idx === this.currentIndex && this.isPlaying);
|
||||
|
||||
const numText = row.querySelector('.track-num-text');
|
||||
const playIcon = row.querySelector('.track-play-icon');
|
||||
|
||||
if (idx === this.currentIndex) {
|
||||
if (numText) numText.classList.add('hidden');
|
||||
if (playIcon) playIcon.classList.remove('hidden');
|
||||
if (playIcon) {
|
||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||
if (window.oxiIcon) {
|
||||
playIcon.outerHTML = window.oxiIcon(iconName, 'track-play-icon');
|
||||
} else {
|
||||
playIcon.className = `fas fa-${iconName} track-play-icon`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (numText) numText.classList.remove('hidden');
|
||||
if (playIcon) playIcon.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const player = document.getElementById('music-player');
|
||||
if (player) {
|
||||
player.classList.toggle('has-track', !!this.currentTrack);
|
||||
}
|
||||
},
|
||||
|
||||
_updateQueueUI() {
|
||||
const queueList = document.getElementById('player-queue-list');
|
||||
if (!queueList) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (this.queue.length === 0) {
|
||||
queueList.innerHTML = `
|
||||
<div class="player-queue-empty">
|
||||
<i class="fas fa-music"></i>
|
||||
<p>${t('music.queue_empty', 'Queue is empty')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
queueList.innerHTML = this.queue.map((track, idx) => `
|
||||
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
|
||||
<span class="queue-item-num">${idx + 1}</span>
|
||||
<span class="queue-item-info">
|
||||
<span class="queue-item-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
|
||||
<span class="queue-item-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
|
||||
</span>
|
||||
<span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span>
|
||||
<button class="queue-item-remove" data-idx="${idx}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
queueList.querySelectorAll('.player-queue-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.queue-item-remove')) return;
|
||||
const idx = parseInt(item.dataset.idx);
|
||||
this.playTrack(idx);
|
||||
});
|
||||
});
|
||||
|
||||
queueList.querySelectorAll('.queue-item-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(btn.dataset.idx);
|
||||
this._removeFromQueue(idx);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_removeFromQueue(idx) {
|
||||
if (idx === this.currentIndex) {
|
||||
if (this.queue.length === 1) {
|
||||
this.audio.pause();
|
||||
this.currentTrack = null;
|
||||
this.currentIndex = -1;
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
this.queue.splice(idx, 1);
|
||||
if (idx < this.currentIndex) {
|
||||
this.currentIndex--;
|
||||
}
|
||||
this._updateQueueUI();
|
||||
this._updateUI();
|
||||
},
|
||||
|
||||
_toggleQueue(show) {
|
||||
const queue = document.getElementById('player-queue');
|
||||
if (queue) {
|
||||
if (show === undefined) {
|
||||
queue.classList.toggle('hidden');
|
||||
} else {
|
||||
queue.classList.toggle('hidden', !show);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_formatTime(secs) {
|
||||
if (!secs || isNaN(secs)) return '0:00';
|
||||
const mins = Math.floor(secs / 60);
|
||||
const s = Math.floor(secs % 60);
|
||||
return `${mins}:${s.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
_formatDuration(secs) {
|
||||
return this._formatTime(secs);
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
musicPlayer.init();
|
||||
});
|
||||
|
||||
window.musicView = musicView;
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "الأخيرة",
|
||||
"favorites": "المفضلة",
|
||||
"photos": "الصور",
|
||||
"music": "الموسيقى",
|
||||
"trash": "سلة المهملات"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "شهر",
|
||||
"view_yearly": "سنة"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "إنشاء قائمة تشغيل",
|
||||
"playlists": "قوائم التشغيل",
|
||||
"no_playlists": "لا توجد قوائم تشغيل بعد",
|
||||
"select_playlist": "اختر قائمة تشغيل",
|
||||
"select_hint": "اختر قائمة تشغيل من الشريط الجانبي أو أنشئ واحدة جديدة",
|
||||
"add_tracks": "إضافة مقاطع",
|
||||
"no_tracks": "لا توجد مقاطع في هذه القائمة",
|
||||
"unknown_artist": "فنان غير معروف",
|
||||
"unknown_title": "غير معروف",
|
||||
"confirm_delete": "هل تريد حذف هذه القائمة؟",
|
||||
"playlist_name": "اسم القائمة",
|
||||
"create": "إنشاء",
|
||||
"delete": "حذف",
|
||||
"share": "مشاركة",
|
||||
"edit": "تعديل",
|
||||
"play_all": "تشغيل الكل",
|
||||
"shuffle": "عشوائي",
|
||||
"repeat": "تكرار",
|
||||
"repeat_one": "تكرار واحد",
|
||||
"queue": "قائمة الانتظار",
|
||||
"queue_empty": "قائمة الانتظار فارغة",
|
||||
"not_playing": "لا يتم التشغيل",
|
||||
"play": "تشغيل",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"previous": "السابق",
|
||||
"next": "التالي",
|
||||
"volume": "مستوى الصوت",
|
||||
"mute": "كتم",
|
||||
"unmute": "إلغاء الكتم",
|
||||
"title": "العنوان",
|
||||
"artist": "الفنان",
|
||||
"album": "الألبوم",
|
||||
"tracks": "مقاطع",
|
||||
"add": "إضافة",
|
||||
"added": "تمت الإضافة!",
|
||||
"added_to_playlist": "تمت إضافته إلى القائمة",
|
||||
"add_to_playlist": "إضافة إلى القائمة",
|
||||
"load_error": "خطأ في تحميل القوائم",
|
||||
"add_error": "تعذر إضافة المقاطع",
|
||||
"no_playlists_yet": "لا توجد قوائم بعد. أنشئ واحدة أولاً!",
|
||||
"selected_files": "محدد:",
|
||||
"error": "خطأ"
|
||||
},
|
||||
"actions": {
|
||||
"search": "البحث في الملفات...",
|
||||
"new_folder": "مجلد جديد",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Zuletzt verwendet",
|
||||
"favorites": "Favoriten",
|
||||
"photos": "Fotos",
|
||||
"music": "Musik",
|
||||
"trash": "Papierkorb"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Monat",
|
||||
"view_yearly": "Jahr"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Playlist erstellen",
|
||||
"playlists": "Playlists",
|
||||
"no_playlists": "Noch keine Playlists",
|
||||
"select_playlist": "Playlist auswählen",
|
||||
"select_hint": "Wählen Sie eine Playlist aus der Seitenleiste oder erstellen Sie eine neue",
|
||||
"add_tracks": "Titel hinzufügen",
|
||||
"no_tracks": "Keine Titel in dieser Playlist",
|
||||
"unknown_artist": "Unbekannter Künstler",
|
||||
"unknown_title": "Unbekannt",
|
||||
"confirm_delete": "Diese Playlist löschen?",
|
||||
"playlist_name": "Playlist-Name",
|
||||
"create": "Erstellen",
|
||||
"delete": "Löschen",
|
||||
"share": "Teilen",
|
||||
"edit": "Bearbeiten",
|
||||
"play_all": "Alle abspielen",
|
||||
"shuffle": "Zufällig",
|
||||
"repeat": "Wiederholen",
|
||||
"repeat_one": "Einen wiederholen",
|
||||
"queue": "Warteschlange",
|
||||
"queue_empty": "Warteschlange ist leer",
|
||||
"not_playing": "Nicht abspielend",
|
||||
"play": "Abspielen",
|
||||
"pause": "Pause",
|
||||
"previous": "Zurück",
|
||||
"next": "Weiter",
|
||||
"volume": "Lautstärke",
|
||||
"mute": "Stumm",
|
||||
"unmute": "Ton ein",
|
||||
"title": "Titel",
|
||||
"artist": "Künstler",
|
||||
"album": "Album",
|
||||
"tracks": "Titel",
|
||||
"add": "Hinzufügen",
|
||||
"added": "Hinzugefügt!",
|
||||
"added_to_playlist": "zur Playlist hinzugefügt",
|
||||
"add_to_playlist": "Zur Playlist hinzufügen",
|
||||
"load_error": "Fehler beim Laden der Playlists",
|
||||
"add_error": "Tracks konnten nicht hinzugefügt werden",
|
||||
"no_playlists_yet": "Noch keine Playlists. Erstellen Sie zuerst eine!",
|
||||
"selected_files": "Ausgewählt:",
|
||||
"error": "Fehler"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Dateien suchen...",
|
||||
"new_folder": "Neuer Ordner",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Recent",
|
||||
"favorites": "Favorites",
|
||||
"photos": "Photos",
|
||||
"music": "Music",
|
||||
"trash": "Trash"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Month",
|
||||
"view_yearly": "Year"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Create Playlist",
|
||||
"playlists": "Playlists",
|
||||
"no_playlists": "No playlists yet",
|
||||
"select_playlist": "Select a playlist",
|
||||
"select_hint": "Choose a playlist from the sidebar or create a new one",
|
||||
"add_tracks": "Add Tracks",
|
||||
"add_to_playlist": "Add to Playlist",
|
||||
"add": "Add",
|
||||
"added": "Added!",
|
||||
"added_to_playlist": "added to playlist",
|
||||
"load_error": "Error loading playlists",
|
||||
"add_error": "Could not add tracks to playlist",
|
||||
"no_playlists_yet": "No playlists yet. Create one first!",
|
||||
"selected_files": "Selected:",
|
||||
"no_tracks": "No tracks in this playlist",
|
||||
"unknown_artist": "Unknown Artist",
|
||||
"unknown_title": "Unknown",
|
||||
"confirm_delete": "Delete this playlist?",
|
||||
"playlist_name": "Playlist name",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"share": "Share",
|
||||
"edit": "Edit",
|
||||
"play_all": "Play All",
|
||||
"shuffle": "Shuffle",
|
||||
"repeat": "Repeat",
|
||||
"repeat_one": "Repeat One",
|
||||
"queue": "Queue",
|
||||
"queue_empty": "Queue is empty",
|
||||
"not_playing": "Not playing",
|
||||
"play": "Play",
|
||||
"pause": "Pause",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"volume": "Volume",
|
||||
"mute": "Mute",
|
||||
"unmute": "Unmute",
|
||||
"title": "Title",
|
||||
"artist": "Artist",
|
||||
"album": "Album",
|
||||
"tracks": "tracks",
|
||||
"error": "Error"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Search files...",
|
||||
"new_folder": "New folder",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Recientes",
|
||||
"favorites": "Favoritos",
|
||||
"photos": "Fotos",
|
||||
"music": "Música",
|
||||
"trash": "Papelera"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Mes",
|
||||
"view_yearly": "Año"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Crear Lista",
|
||||
"playlists": "Listas",
|
||||
"no_playlists": "Sin listas aún",
|
||||
"select_playlist": "Selecciona una lista",
|
||||
"select_hint": "Elige una lista de la barra lateral o crea una nueva",
|
||||
"add_tracks": "Añadir Pistas",
|
||||
"no_tracks": "No hay pistas en esta lista",
|
||||
"unknown_artist": "Artista Desconocido",
|
||||
"unknown_title": "Desconocido",
|
||||
"confirm_delete": "¿Eliminar esta lista?",
|
||||
"playlist_name": "Nombre de la lista",
|
||||
"create": "Crear",
|
||||
"delete": "Eliminar",
|
||||
"share": "Compartir",
|
||||
"edit": "Editar",
|
||||
"play_all": "Reproducir Todo",
|
||||
"shuffle": "Aleatorio",
|
||||
"repeat": "Repetir",
|
||||
"repeat_one": "Repetir Una",
|
||||
"queue": "Cola",
|
||||
"queue_empty": "Cola vacía",
|
||||
"not_playing": "No reproduciendo",
|
||||
"play": "Reproducir",
|
||||
"pause": "Pausar",
|
||||
"previous": "Anterior",
|
||||
"next": "Siguiente",
|
||||
"volume": "Volumen",
|
||||
"mute": "Silenciar",
|
||||
"unmute": "Activar sonido",
|
||||
"title": "Título",
|
||||
"artist": "Artista",
|
||||
"album": "Álbum",
|
||||
"tracks": "pistas",
|
||||
"add": "Añadir",
|
||||
"added": "¡Añadido!",
|
||||
"added_to_playlist": "añadido a la lista",
|
||||
"add_to_playlist": "Añadir a playlist",
|
||||
"load_error": "Error al cargar listas",
|
||||
"add_error": "No se pudieron añadir las pistas",
|
||||
"no_playlists_yet": "No hay listas aún. ¡Crea una primero!",
|
||||
"selected_files": "Seleccionados:",
|
||||
"error": "Error"
|
||||
},
|
||||
"share": {
|
||||
"dialogTitle": "Compartir Enlace",
|
||||
"linkLabel": "Enlace compartido:",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "اخیر",
|
||||
"favorites": "موردعلاقهها",
|
||||
"photos": "عکسها",
|
||||
"music": "موسیقی",
|
||||
"trash": "سطل زباله"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "ماه",
|
||||
"view_yearly": "سال"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "ایجاد فهرست پخش",
|
||||
"playlists": "فهرستهای پخش",
|
||||
"no_playlists": "هنوز فهرست پخشی نیست",
|
||||
"select_playlist": "یک فهرست پخش انتخاب کنید",
|
||||
"select_hint": "از نوار کناری یک فهرست پخش انتخاب کنید یا یکی جدید بسازید",
|
||||
"add_tracks": "افزودن آهنگها",
|
||||
"no_tracks": "هیچ آهنگی در این فهرست پخش نیست",
|
||||
"unknown_artist": "هنرمند ناشناس",
|
||||
"unknown_title": "ناشناس",
|
||||
"confirm_delete": "این فهرست پخش حذف شود؟",
|
||||
"playlist_name": "نام فهرست پخش",
|
||||
"create": "ایجاد",
|
||||
"delete": "حذف",
|
||||
"share": "همرسانی",
|
||||
"edit": "ویرایش",
|
||||
"play_all": "پخش همه",
|
||||
"shuffle": "تصادفی",
|
||||
"repeat": "تکرار",
|
||||
"repeat_one": "تکرار یک",
|
||||
"queue": "صف",
|
||||
"queue_empty": "صف خالی است",
|
||||
"not_playing": "در حال پخش نیست",
|
||||
"play": "پخش",
|
||||
"pause": "توقف",
|
||||
"previous": "قبلی",
|
||||
"next": "بعدی",
|
||||
"volume": "صدا",
|
||||
"mute": "بیصدا",
|
||||
"unmute": "صدا فعال",
|
||||
"title": "عنوان",
|
||||
"artist": "هنرمند",
|
||||
"album": "آلبوم",
|
||||
"tracks": "آهنگ",
|
||||
"add": "افزودن",
|
||||
"added": "افزوده شد!",
|
||||
"added_to_playlist": "به فهرست پخش افزوده شد",
|
||||
"add_to_playlist": "افزودن به فهرست پخش",
|
||||
"load_error": "خطا در بارگیری فهرست پخش",
|
||||
"add_error": "امکان افزودن آهنگها به فهرست پخش نیست",
|
||||
"no_playlists_yet": "فهرست پخشی وجود ندارد. اول یکی بسازید!",
|
||||
"selected_files": "انتخاب شده:",
|
||||
"error": "خطا"
|
||||
},
|
||||
"actions": {
|
||||
"search": "جستوجوی پروندهها..",
|
||||
"new_folder": "پوشهٔ جدید",
|
||||
@@ -523,5 +568,18 @@
|
||||
"error_create_pw": "ایجاد رمز ناموفق بود",
|
||||
"confirm_revoke": "رمز «{{label}}» ابطال شود؟ کلاینتها از کار میافتند.",
|
||||
"error_revoke": "ابطال ناموفق بود"
|
||||
},
|
||||
"notifications": {
|
||||
"file_renamed": "فایل تغییر نام داد",
|
||||
"file_renamed_to": "فایل به \"{{name}}\" تغییر نام داد",
|
||||
"folder_renamed": "پوشه تغییر نام داد",
|
||||
"folder_renamed_to": "پوشه به \"{{name}}\" تغییر نام داد",
|
||||
"file_uploaded": "فایل آپلود شد",
|
||||
"file_deleted": "فایل به زبالهدان منتقل شد",
|
||||
"folder_deleted": "پوشه به زبالهدان منتقل شد",
|
||||
"item_deleted_permanently": "آیتم برای همیشه حذف شد",
|
||||
"trash_emptied": "زبالهدان خالی شد",
|
||||
"title": "اعلانها",
|
||||
"empty": "بدون اعلان"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Récents",
|
||||
"favorites": "Favoris",
|
||||
"photos": "Photos",
|
||||
"music": "Musique",
|
||||
"trash": "Corbeille"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Mois",
|
||||
"view_yearly": "Année"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Créer une Playlist",
|
||||
"playlists": "Playlists",
|
||||
"no_playlists": "Aucune playlist",
|
||||
"select_playlist": "Sélectionnez une playlist",
|
||||
"select_hint": "Choisissez une playlist dans la barre latérale ou créez-en une nouvelle",
|
||||
"add_tracks": "Ajouter des Pistes",
|
||||
"no_tracks": "Aucune piste dans cette playlist",
|
||||
"unknown_artist": "Artiste Inconnu",
|
||||
"unknown_title": "Inconnu",
|
||||
"confirm_delete": "Supprimer cette playlist ?",
|
||||
"playlist_name": "Nom de la playlist",
|
||||
"create": "Créer",
|
||||
"delete": "Supprimer",
|
||||
"share": "Partager",
|
||||
"edit": "Modifier",
|
||||
"play_all": "Tout Lire",
|
||||
"shuffle": "Aléatoire",
|
||||
"repeat": "Répéter",
|
||||
"repeat_one": "Répéter Une",
|
||||
"queue": "File d'attente",
|
||||
"queue_empty": "File d'attente vide",
|
||||
"not_playing": "Pas en lecture",
|
||||
"play": "Lecture",
|
||||
"pause": "Pause",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"volume": "Volume",
|
||||
"mute": "Muet",
|
||||
"unmute": "Activer le son",
|
||||
"title": "Titre",
|
||||
"artist": "Artiste",
|
||||
"album": "Album",
|
||||
"tracks": "pistes",
|
||||
"add": "Ajouter",
|
||||
"added": "Ajouté !",
|
||||
"added_to_playlist": "ajouté à la playlist",
|
||||
"add_to_playlist": "Ajouter à la playlist",
|
||||
"load_error": "Erreur de chargement des playlists",
|
||||
"add_error": "Impossible d'ajouter les pistes",
|
||||
"no_playlists_yet": "Pas encore de playlists. Créez-en une d'abord !",
|
||||
"selected_files": "Sélectionnés :",
|
||||
"error": "Erreur"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Rechercher des fichiers...",
|
||||
"new_folder": "Nouveau dossier",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "हाल ही में",
|
||||
"favorites": "पसंदीदा",
|
||||
"photos": "फ़ोटो",
|
||||
"music": "संगीत",
|
||||
"trash": "रद्दी"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "महीना",
|
||||
"view_yearly": "वर्ष"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "प्लेलिस्ट बनाएँ",
|
||||
"playlists": "प्लेलिस्ट",
|
||||
"no_playlists": "अभी कोई प्लेलिस्ट नहीं",
|
||||
"select_playlist": "प्लेलिस्ट चुनें",
|
||||
"select_hint": "साइडबार से प्लेलिस्ट चुनें या नई बनाएँ",
|
||||
"add_tracks": "ट्रैक जोड़ें",
|
||||
"no_tracks": "इस प्लेलिस्ट में कोई ट्रैक नहीं",
|
||||
"unknown_artist": "अज्ञात कलाकार",
|
||||
"unknown_title": "अज्ञात",
|
||||
"confirm_delete": "इस प्लेलिस्ट को हटाएँ?",
|
||||
"playlist_name": "प्लेलिस्ट का नाम",
|
||||
"create": "बनाएँ",
|
||||
"delete": "हटाएँ",
|
||||
"share": "साझा करें",
|
||||
"edit": "संपादित करें",
|
||||
"play_all": "सभी चलाएँ",
|
||||
"shuffle": "शफल",
|
||||
"repeat": "दोहराएँ",
|
||||
"repeat_one": "एक दोहराएँ",
|
||||
"queue": "कतार",
|
||||
"queue_empty": "कतार खाली है",
|
||||
"not_playing": "नहीं चल रहा",
|
||||
"play": "चलाएँ",
|
||||
"pause": "रोकें",
|
||||
"previous": "पिछला",
|
||||
"next": "अगला",
|
||||
"volume": "आवाज़",
|
||||
"mute": "म्यूट",
|
||||
"unmute": "अनम्यूट",
|
||||
"title": "शीर्षक",
|
||||
"artist": "कलाकार",
|
||||
"album": "एल्बम",
|
||||
"tracks": "ट्रैक",
|
||||
"add": "जोड़ें",
|
||||
"added": "जोड़ा गया!",
|
||||
"added_to_playlist": "प्लेलिस्ट में जोड़ा गया",
|
||||
"add_to_playlist": "प्लेलिस्ट में जोड़ें",
|
||||
"load_error": "प्लेलिस्ट लोड करने में त्रुटि",
|
||||
"add_error": "प्लेलिस्ट में ट्रैक नहीं जोड़े जा सके",
|
||||
"no_playlists_yet": "अभी तक कोई प्लेलिस्ट नहीं। पहले एक बनाएं!",
|
||||
"selected_files": "चयनित:",
|
||||
"error": "त्रुटि"
|
||||
},
|
||||
"actions": {
|
||||
"search": "फ़ाइलें खोजें...",
|
||||
"new_folder": "नया फ़ोल्डर",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Recenti",
|
||||
"favorites": "Preferiti",
|
||||
"photos": "Foto",
|
||||
"music": "Musica",
|
||||
"trash": "Cestino"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Mese",
|
||||
"view_yearly": "Anno"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Crea Playlist",
|
||||
"playlists": "Playlist",
|
||||
"no_playlists": "Nessuna playlist",
|
||||
"select_playlist": "Seleziona una playlist",
|
||||
"select_hint": "Scegli una playlist dalla barra laterale o creane una nuova",
|
||||
"add_tracks": "Aggiungi Tracce",
|
||||
"no_tracks": "Nessuna traccia in questa playlist",
|
||||
"unknown_artist": "Artista Sconosciuto",
|
||||
"unknown_title": "Sconosciuto",
|
||||
"confirm_delete": "Eliminare questa playlist?",
|
||||
"playlist_name": "Nome playlist",
|
||||
"create": "Crea",
|
||||
"delete": "Elimina",
|
||||
"share": "Condividi",
|
||||
"edit": "Modifica",
|
||||
"play_all": "Riproduci Tutto",
|
||||
"shuffle": "Casuale",
|
||||
"repeat": "Ripeti",
|
||||
"repeat_one": "Ripeti Una",
|
||||
"queue": "Coda",
|
||||
"queue_empty": "Coda vuota",
|
||||
"not_playing": "Non in riproduzione",
|
||||
"play": "Riproduci",
|
||||
"pause": "Pausa",
|
||||
"previous": "Precedente",
|
||||
"next": "Successivo",
|
||||
"volume": "Volume",
|
||||
"mute": "Muto",
|
||||
"unmute": "Attiva audio",
|
||||
"title": "Titolo",
|
||||
"artist": "Artista",
|
||||
"album": "Album",
|
||||
"tracks": "tracce",
|
||||
"add": "Aggiungi",
|
||||
"added": "Aggiunto!",
|
||||
"added_to_playlist": "aggiunto alla playlist",
|
||||
"add_to_playlist": "Aggiungi alla playlist",
|
||||
"load_error": "Errore nel caricamento delle playlist",
|
||||
"add_error": "Impossibile aggiungere le tracce",
|
||||
"no_playlists_yet": "Nessuna playlist ancora. Creane una prima!",
|
||||
"selected_files": "Selezionati:",
|
||||
"error": "Errore"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Cerca file...",
|
||||
"new_folder": "Nuova cartella",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "最近",
|
||||
"favorites": "お気に入り",
|
||||
"photos": "写真",
|
||||
"music": "音楽",
|
||||
"trash": "ゴミ箱"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "月",
|
||||
"view_yearly": "年"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "プレイリストを作成",
|
||||
"playlists": "プレイリスト",
|
||||
"no_playlists": "プレイリストがありません",
|
||||
"select_playlist": "プレイリストを選択",
|
||||
"select_hint": "サイドバーからプレイリストを選択するか、新しいものを作成してください",
|
||||
"add_tracks": "トラックを追加",
|
||||
"no_tracks": "このプレイリストにトラックがありません",
|
||||
"unknown_artist": "不明なアーティスト",
|
||||
"unknown_title": "不明",
|
||||
"confirm_delete": "このプレイリストを削除しますか?",
|
||||
"playlist_name": "プレイリスト名",
|
||||
"create": "作成",
|
||||
"delete": "削除",
|
||||
"share": "共有",
|
||||
"edit": "編集",
|
||||
"play_all": "すべて再生",
|
||||
"shuffle": "シャッフル",
|
||||
"repeat": "リピート",
|
||||
"repeat_one": "1曲リピート",
|
||||
"queue": "キュー",
|
||||
"queue_empty": "キューが空です",
|
||||
"not_playing": "再生していません",
|
||||
"play": "再生",
|
||||
"pause": "一時停止",
|
||||
"previous": "前へ",
|
||||
"next": "次へ",
|
||||
"volume": "音量",
|
||||
"mute": "ミュート",
|
||||
"unmute": "ミュート解除",
|
||||
"title": "タイトル",
|
||||
"artist": "アーティスト",
|
||||
"album": "アルバム",
|
||||
"tracks": "曲",
|
||||
"add": "追加",
|
||||
"added": "追加しました!",
|
||||
"added_to_playlist": "プレイリストに追加しました",
|
||||
"add_to_playlist": "プレイリストに追加",
|
||||
"load_error": "プレイリストの読み込みエラー",
|
||||
"add_error": "曲を追加できませんでした",
|
||||
"no_playlists_yet": "プレイリストがありません。最初に作成してください!",
|
||||
"selected_files": "選択中:",
|
||||
"error": "エラー"
|
||||
},
|
||||
"actions": {
|
||||
"search": "ファイルを検索...",
|
||||
"new_folder": "新しいフォルダ",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "최근",
|
||||
"favorites": "즐겨찾기",
|
||||
"photos": "사진",
|
||||
"music": "음악",
|
||||
"trash": "휴지통"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "월",
|
||||
"view_yearly": "년"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "재생목록 만들기",
|
||||
"playlists": "재생목록",
|
||||
"no_playlists": "아직 재생목록이 없습니다",
|
||||
"select_playlist": "재생목록 선택",
|
||||
"select_hint": "사이드바에서 재생목록을 선택하거나 새로 만드세요",
|
||||
"add_tracks": "트랙 추가",
|
||||
"no_tracks": "이 재생목록에 트랙이 없습니다",
|
||||
"unknown_artist": "알 수 없는 아티스트",
|
||||
"unknown_title": "알 수 없음",
|
||||
"confirm_delete": "이 재생목록을 삭제하시겠습니까?",
|
||||
"playlist_name": "재생목록 이름",
|
||||
"create": "만들기",
|
||||
"delete": "삭제",
|
||||
"share": "공유",
|
||||
"edit": "편집",
|
||||
"play_all": "전체 재생",
|
||||
"shuffle": "셔플",
|
||||
"repeat": "반복",
|
||||
"repeat_one": "한 곡 반복",
|
||||
"queue": "대기열",
|
||||
"queue_empty": "대기열이 비어 있습니다",
|
||||
"not_playing": "재생 중이 아닙니다",
|
||||
"play": "재생",
|
||||
"pause": "일시정지",
|
||||
"previous": "이전",
|
||||
"next": "다음",
|
||||
"volume": "볼륨",
|
||||
"mute": "음소거",
|
||||
"unmute": "음소거 해제",
|
||||
"title": "제목",
|
||||
"artist": "아티스트",
|
||||
"album": "앨범",
|
||||
"tracks": "개 트랙",
|
||||
"add": "추가",
|
||||
"added": "추가됨!",
|
||||
"added_to_playlist": "플레이리스트에 추가됨",
|
||||
"add_to_playlist": "플레이리스트에 추가",
|
||||
"load_error": "플레이리스트 로드 오류",
|
||||
"add_error": "트랙을 플레이리스트에 추가할 수 없습니다",
|
||||
"no_playlists_yet": "플레이리스트가 없습니다. 먼저 하나를 만드세요!",
|
||||
"selected_files": "선택됨:",
|
||||
"error": "오류"
|
||||
},
|
||||
"actions": {
|
||||
"search": "파일 검색...",
|
||||
"new_folder": "새 폴더",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Recente",
|
||||
"favorites": "Favorieten",
|
||||
"photos": "Foto's",
|
||||
"music": "Muziek",
|
||||
"trash": "Prullenbak"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Maand",
|
||||
"view_yearly": "Jaar"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Afspeellijst Maken",
|
||||
"playlists": "Afspeellijsten",
|
||||
"no_playlists": "Nog geen afspeellijsten",
|
||||
"select_playlist": "Selecteer een afspeellijst",
|
||||
"select_hint": "Kies een afspeellijst uit de zijbalk of maak een nieuwe",
|
||||
"add_tracks": "Tracks Toevoegen",
|
||||
"no_tracks": "Geen tracks in deze afspeellijst",
|
||||
"unknown_artist": "Onbekende Artiest",
|
||||
"unknown_title": "Onbekend",
|
||||
"confirm_delete": "Deze afspeellijst verwijderen?",
|
||||
"playlist_name": "Naam afspeellijst",
|
||||
"create": "Maken",
|
||||
"delete": "Verwijderen",
|
||||
"share": "Delen",
|
||||
"edit": "Bewerken",
|
||||
"play_all": "Alles Afspelen",
|
||||
"shuffle": "Shuffle",
|
||||
"repeat": "Herhalen",
|
||||
"repeat_one": "Een Herhalen",
|
||||
"queue": "Wachtrij",
|
||||
"queue_empty": "Wachtrij is leeg",
|
||||
"not_playing": "Niet afspelend",
|
||||
"play": "Afspelen",
|
||||
"pause": "Pauzeren",
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende",
|
||||
"volume": "Volume",
|
||||
"mute": "Dempen",
|
||||
"unmute": "Geluid aan",
|
||||
"title": "Titel",
|
||||
"artist": "Artiest",
|
||||
"album": "Album",
|
||||
"tracks": "tracks",
|
||||
"add": "Toevoegen",
|
||||
"added": "Toegevoegd!",
|
||||
"added_to_playlist": "toegevoegd aan playlist",
|
||||
"add_to_playlist": "Aan playlist toevoegen",
|
||||
"load_error": "Fout bij laden van playlists",
|
||||
"add_error": "Kon tracks niet toevoegen aan playlist",
|
||||
"no_playlists_yet": "Nog geen playlists. Maak er eerst een!",
|
||||
"selected_files": "Geselecteerd:",
|
||||
"error": "Fout"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Zoek bestanden...",
|
||||
"new_folder": "Nieuwe map",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Recentes",
|
||||
"favorites": "Favoritos",
|
||||
"photos": "Fotos",
|
||||
"music": "Música",
|
||||
"trash": "Lixeira"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Mês",
|
||||
"view_yearly": "Ano"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Criar Playlist",
|
||||
"playlists": "Playlists",
|
||||
"no_playlists": "Nenhuma playlist ainda",
|
||||
"select_playlist": "Selecione uma playlist",
|
||||
"select_hint": "Escolha uma playlist na barra lateral ou crie uma nova",
|
||||
"add_tracks": "Adicionar Faixas",
|
||||
"no_tracks": "Nenhuma faixa nesta playlist",
|
||||
"unknown_artist": "Artista Desconhecido",
|
||||
"unknown_title": "Desconhecido",
|
||||
"confirm_delete": "Excluir esta playlist?",
|
||||
"playlist_name": "Nome da playlist",
|
||||
"create": "Criar",
|
||||
"delete": "Excluir",
|
||||
"share": "Compartilhar",
|
||||
"edit": "Editar",
|
||||
"play_all": "Reproduzir Tudo",
|
||||
"shuffle": "Aleatório",
|
||||
"repeat": "Repetir",
|
||||
"repeat_one": "Repetir Uma",
|
||||
"queue": "Fila",
|
||||
"queue_empty": "Fila vazia",
|
||||
"not_playing": "Não reproduzindo",
|
||||
"play": "Reproduzir",
|
||||
"pause": "Pausar",
|
||||
"previous": "Anterior",
|
||||
"next": "Próximo",
|
||||
"volume": "Volume",
|
||||
"mute": "Mudo",
|
||||
"unmute": "Ativar som",
|
||||
"title": "Título",
|
||||
"artist": "Artista",
|
||||
"album": "Álbum",
|
||||
"tracks": "faixas",
|
||||
"add": "Adicionar",
|
||||
"added": "Adicionado!",
|
||||
"added_to_playlist": "adicionado à playlist",
|
||||
"add_to_playlist": "Adicionar à playlist",
|
||||
"load_error": "Erro ao carregar playlists",
|
||||
"add_error": "Não foi possível adicionar as faixas",
|
||||
"no_playlists_yet": "Nenhuma playlist ainda. Crie uma primeiro!",
|
||||
"selected_files": "Selecionados:",
|
||||
"error": "Erro"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Pesquisar arquivos...",
|
||||
"new_folder": "Nova pasta",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "Недавние",
|
||||
"favorites": "Избранное",
|
||||
"photos": "Фото",
|
||||
"music": "Музыка",
|
||||
"trash": "Корзина"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "Месяц",
|
||||
"view_yearly": "Год"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "Создать плейлист",
|
||||
"playlists": "Плейлисты",
|
||||
"no_playlists": "Плейлистов пока нет",
|
||||
"select_playlist": "Выберите плейлист",
|
||||
"select_hint": "Выберите плейлист на панели слева или создайте новый",
|
||||
"add_tracks": "Добавить треки",
|
||||
"no_tracks": "В этом плейлисте нет треков",
|
||||
"unknown_artist": "Неизвестный исполнитель",
|
||||
"unknown_title": "Неизвестно",
|
||||
"confirm_delete": "Удалить этот плейлист?",
|
||||
"playlist_name": "Название плейлиста",
|
||||
"create": "Создать",
|
||||
"delete": "Удалить",
|
||||
"share": "Поделиться",
|
||||
"edit": "Редактировать",
|
||||
"play_all": "Воспроизвести все",
|
||||
"shuffle": "Перемешать",
|
||||
"repeat": "Повтор",
|
||||
"repeat_one": "Повторять один",
|
||||
"queue": "Очередь",
|
||||
"queue_empty": "Очередь пуста",
|
||||
"not_playing": "Ничего не играет",
|
||||
"play": "Воспроизвести",
|
||||
"pause": "Пауза",
|
||||
"previous": "Предыдущий",
|
||||
"next": "Следующий",
|
||||
"volume": "Громкость",
|
||||
"mute": "Выключить звук",
|
||||
"unmute": "Включить звук",
|
||||
"title": "Название",
|
||||
"artist": "Исполнитель",
|
||||
"album": "Альбом",
|
||||
"tracks": "треков",
|
||||
"add": "Добавить",
|
||||
"added": "Добавлено!",
|
||||
"added_to_playlist": "добавлен в плейлист",
|
||||
"add_to_playlist": "Добавить в плейлист",
|
||||
"load_error": "Ошибка загрузки плейлистов",
|
||||
"add_error": "Не удалось добавить треки в плейлист",
|
||||
"no_playlists_yet": "Плейлистов пока нет. Создайте сначала!",
|
||||
"selected_files": "Выбрано:",
|
||||
"error": "Ошибка"
|
||||
},
|
||||
"actions": {
|
||||
"search": "Поиск файлов...",
|
||||
"new_folder": "Новая папка",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"recent": "最近",
|
||||
"favorites": "收藏",
|
||||
"photos": "照片",
|
||||
"music": "音乐",
|
||||
"trash": "回收站"
|
||||
},
|
||||
"photos": {
|
||||
@@ -19,6 +20,50 @@
|
||||
"view_monthly": "月",
|
||||
"view_yearly": "年"
|
||||
},
|
||||
"music": {
|
||||
"create_playlist": "创建播放列表",
|
||||
"playlists": "播放列表",
|
||||
"no_playlists": "还没有播放列表",
|
||||
"select_playlist": "选择一个播放列表",
|
||||
"select_hint": "从侧边栏选择播放列表或创建新播放列表",
|
||||
"add_tracks": "添加曲目",
|
||||
"no_tracks": "此播放列表中没有曲目",
|
||||
"unknown_artist": "未知艺术家",
|
||||
"unknown_title": "未知",
|
||||
"confirm_delete": "删除此播放列表?",
|
||||
"playlist_name": "播放列表名称",
|
||||
"create": "创建",
|
||||
"delete": "删除",
|
||||
"share": "分享",
|
||||
"edit": "编辑",
|
||||
"play_all": "全部播放",
|
||||
"shuffle": "随机播放",
|
||||
"repeat": "重复",
|
||||
"repeat_one": "单曲循环",
|
||||
"queue": "播放队列",
|
||||
"queue_empty": "播放队列为空",
|
||||
"not_playing": "未播放",
|
||||
"play": "播放",
|
||||
"pause": "暂停",
|
||||
"previous": "上一首",
|
||||
"next": "下一首",
|
||||
"volume": "音量",
|
||||
"mute": "静音",
|
||||
"unmute": "取消静音",
|
||||
"title": "标题",
|
||||
"artist": "艺术家",
|
||||
"album": "专辑",
|
||||
"tracks": "首曲目",
|
||||
"add": "添加",
|
||||
"added": "已添加!",
|
||||
"added_to_playlist": "已添加到播放列表",
|
||||
"add_to_playlist": "添加到播放列表",
|
||||
"load_error": "加载播放列表出错",
|
||||
"add_error": "无法将曲目添加到播放列表",
|
||||
"no_playlists_yet": "暂无播放列表。请先创建一个!",
|
||||
"selected_files": "已选择:",
|
||||
"error": "错误"
|
||||
},
|
||||
"actions": {
|
||||
"search": "搜索文件...",
|
||||
"new_folder": "新建文件夹",
|
||||
@@ -485,5 +530,18 @@
|
||||
"error_create_pw": "创建应用密码失败",
|
||||
"confirm_revoke": "撤销应用密码\"{{label}}\"?使用此密码的客户端将停止工作。",
|
||||
"error_revoke": "撤销失败"
|
||||
},
|
||||
"notifications": {
|
||||
"file_renamed": "文件已重命名",
|
||||
"file_renamed_to": "文件已重命名为\"{{name}}\"",
|
||||
"folder_renamed": "文件夹已重命名",
|
||||
"folder_renamed_to": "文件夹已重命名为\"{{name}}\"",
|
||||
"file_uploaded": "文件已上传",
|
||||
"file_deleted": "文件已移至回收站",
|
||||
"folder_deleted": "文件夹已移至回收站",
|
||||
"item_deleted_permanently": "项目已永久删除",
|
||||
"trash_emptied": "回收站已清空",
|
||||
"title": "通知",
|
||||
"empty": "暂无通知"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user