feat(music): mini player ekle

Müzik çalar durumunu yönetmek için global store oluştur.
Özel bir mini player bileşeni ile çalma listesi ve kontrolleri ekle.
Müzik çaların uygulama genelinde kalıcı olmasını sağla.
This commit is contained in:
2026-01-18 01:51:15 +03:00
parent c945458a81
commit d5d9184872
4 changed files with 510 additions and 153 deletions

View File

@@ -0,0 +1,246 @@
<script>
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
import { API } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
function thumbnailURL(item) {
if (!item?.thumbnail) return null;
const token = localStorage.getItem("token");
const separator = item.thumbnail.includes("?") ? "&" : "?";
return `${API}${item.thumbnail}${separator}token=${token}`;
}
function formatTime(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, "0")}`;
}
function remainingTime(current, total) {
if (!Number.isFinite(total) || total <= 0) return "-0:00";
const remaining = Math.max(total - (current || 0), 0);
return `-${formatTime(remaining)}`;
}
function sourceLabel(item) {
if (item?.tracker === "youtube" || item?.thumbnail) return "YouTube";
return "Music";
}
</script>
{#if $musicPlayer.currentTrack}
<div class="mini-player" aria-label="Mini music player">
<div class="mini-top">
<div class="mini-user-meta">
<div class="mini-user-name">
{cleanFileName($musicPlayer.currentTrack.title)}
</div>
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
</div>
<div class="mini-actions">
<button class="mini-icon-btn" title="Paylaş">
<i class="fa-solid fa-arrow-up-from-bracket"></i>
</button>
<button class="mini-icon-btn" title="Beğen">
<i class="fa-regular fa-heart"></i>
</button>
</div>
</div>
<div class="mini-cover">
{#if thumbnailURL($musicPlayer.currentTrack)}
<img
src={thumbnailURL($musicPlayer.currentTrack)}
alt={$musicPlayer.currentTrack.title}
/>
{:else}
<div class="mini-cover-placeholder">
<i class="fa-solid fa-music"></i>
</div>
{/if}
</div>
<div class="mini-progress">
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
<div class="mini-bar">
<div
class="mini-bar-fill"
style="width: {($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0}%"
></div>
</div>
<span class="mini-time">
{remainingTime($musicPlayer.currentTime, $musicPlayer.duration)}
</span>
</div>
<div class="mini-controls">
<button class="mini-btn" on:click={playPrevious} title="Önceki">
<i class="fa-solid fa-backward-step"></i>
</button>
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur">
{#if $musicPlayer.isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<button class="mini-btn" on:click={playNext} title="Sonraki">
<i class="fa-solid fa-forward-step"></i>
</button>
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
{/if}
<style>
.mini-player {
position: fixed;
right: 24px;
bottom: 24px;
width: 240px;
padding: 16px;
background: rgba(12, 12, 12, 0.72);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 26px;
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
color: #f6f6f6;
backdrop-filter: blur(14px);
z-index: 50;
}
.mini-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.mini-user-meta {
min-width: 0;
text-align: left;
}
.mini-user-name {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mini-user-handle {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
}
.mini-actions {
display: flex;
gap: 8px;
}
.mini-icon-btn {
width: 32px;
height: 32px;
border-radius: 12px;
border: none;
background: rgba(255, 255, 255, 0.12);
color: #f6f6f6;
cursor: pointer;
}
.mini-cover {
margin: 14px auto 12px;
width: 160px;
height: 160px;
border-radius: 20px;
overflow: hidden;
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
display: grid;
place-items: center;
transform: translateY(-6px);
}
.mini-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.mini-cover-placeholder {
color: rgba(255, 255, 255, 0.45);
font-size: 40px;
}
.mini-progress {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
margin: 8px 0 14px;
}
.mini-time {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
}
.mini-bar {
height: 3px;
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
overflow: hidden;
}
.mini-bar-fill {
height: 100%;
background: var(--yellow);
border-radius: 999px;
}
.mini-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.mini-btn {
width: 34px;
height: 34px;
border-radius: 999px;
border: none;
background: rgba(255, 255, 255, 0.14);
color: #f6f6f6;
cursor: pointer;
}
.mini-btn.main {
width: 44px;
height: 44px;
border-radius: 999px;
background: var(--yellow);
color: #1e1e1e;
}
.mini-btn.ghost {
background: rgba(255, 255, 255, 0.06);
}
@media (max-width: 820px) {
.mini-player {
right: 16px;
bottom: 16px;
width: 220px;
}
.mini-cover {
width: 140px;
height: 140px;
}
}
</style>