From 3736d78dfea695b9ec94519da901ad04bbb37c91 Mon Sep 17 00:00:00 2001 From: abdussamedulutas Date: Wed, 17 Jun 2026 13:38:17 +0300 Subject: [PATCH] =?UTF-8?q?Studio:=20e=C5=9Fle=C5=9Fme=20ak=C4=B1=C5=9F?= =?UTF-8?q?=C4=B1=20tam=20=C3=A7al=C4=B1=C5=9F=C4=B1r=20hale=20getirildi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdk/index.js: - accepted/pair sinyali → mwse.pairs.set(from, peer) - end/pair sinyali → mwse.pairs.delete(from) (İstek gönderen taraf pairs haritasına eklendi) sdk/Peer.js: - acceptPair() başarısında → mwse.pairs.set(socketId, this) - rejectPair() → mwse.pairs.delete(socketId) - endPair() → mwse.pairs.delete(socketId) (Kabul eden taraf da pairs haritasına eklendi) public/studio/Studio.js — tamamen yeniden yazıldı: Gelen eşleme isteği → bildirim banner'ı: - Yeşil arka plan, socket ID kodu olarak gösterilir - [Reddet] → rejectPair() → banner kapanır - [Kabul Et] → acceptPair() → banner kapanır, eşler yenilenir Eşler kolonu: artık mwse.pairs'tan doğru veriler geliyor Oda oluşturma: description alanı eklendi (sunucu zorunlu tutuyordu) Oda oluştur → "Oda adı / Açıklama / Şifre" modal ID ile ara → prompt() yerine modal Araç çubuğu: "Kimliğim" kartı, kopyala butonu (⎘→✓ flash) public/studio/style.css: .mwse-notif-area / .mwse-notif-bar / __msg / __dot / __actions go test -race ./... — yeşil Co-Authored-By: Claude Sonnet 4.6 --- public/studio/Studio.js | 387 +++++++++++++++++++--------------------- public/studio/style.css | 38 ++++ sdk/Peer.js | 4 + sdk/index.js | 3 + 4 files changed, 233 insertions(+), 199 deletions(-) diff --git a/public/studio/Studio.js b/public/studio/Studio.js index 5d2cbb3..9beddae 100644 --- a/public/studio/Studio.js +++ b/public/studio/Studio.js @@ -1,16 +1,17 @@ // MWSE Studio — dahili yönetim arayüzü. -// Miller kolonlar: Giriş → Eşler / Odalar / Cihazlar → Eylemler → Akışlar → Kalite +// Akış: bağlan → ID'yi kopyala → eşle → WebRTC ara / oda oluştur import ColumnView from '/studio/ColumnView.js'; import { MediaSources } from '/sdk/webrtc/index.js'; export default class Studio { constructor(mwse, container) { - this.mwse = mwse; - this._el = typeof container === 'string' + this.mwse = mwse; + this._el = typeof container === 'string' ? document.querySelector(container) : container; - this._view = new ColumnView(this._el); - this._devices = { cameras: [], microphones: [] }; + this._view = new ColumnView(this._el); + this._devices = { cameras: [], microphones: [] }; this._statusEl = null; + this._notifArea = null; this._styleOK = false; this._remoteMedia = []; } @@ -19,26 +20,28 @@ export default class Studio { this._el.classList.add('mwse-studio'); this._injectStyle(); this._buildToolbar(); + this._buildNotifArea(); this._view.mount(); - // Cihazları yükle await this._loadDevices(); - - // Kök kolon this._pushRootColumn(); - // Gelen eşleme isteği bildirimi + // ── Gelen eşleme isteği → bildirim banner'ı ────────────────────── this.mwse.me.on('request/pair', peer => { - this._setStatus('', `${peer.socketId.slice(-8)}… bağlanmak istiyor`); + this._showPairRequest(peer); }); - // Yeni eşleşme → gelen akışları dinle + kolon yenile + // Eşleşme onaylandı (istek gönderen taraf) this.mwse.me.on('accepted/pair', peer => { this._watchIncoming(peer); this._view.refresh(); + this._setStatus('online', `${peer.socketId.slice(-8)} eşleşmesi kuruldu`); }); + // Eşleşme bitti this.mwse.me.on('end/pair', () => this._view.refresh()); + + // Oda değişimi this.mwse.on('room', () => this._view.refresh()); return this; @@ -56,14 +59,14 @@ export default class Studio { logo.innerHTML = 'MWSE Studio'; bar.appendChild(logo); - // Benim ID kartı — kopyalanabilir + // Benim ID kartı const idCard = document.createElement('div'); idCard.className = 'mwse-id-card'; - idCard.title = 'Socket ID\'yi kopyala'; + idCard.title = 'Tıkla → Kopyala'; const idLabel = document.createElement('span'); idLabel.className = 'mwse-id-card__label'; - idLabel.textContent = 'ID'; + idLabel.textContent = 'Kimliğim'; const idValue = document.createElement('span'); idValue.className = 'mwse-id-card__value'; @@ -83,16 +86,14 @@ export default class Studio { setTimeout(() => { idCard.classList.remove('mwse-id-card--copied'); idCopy.textContent = '⎘'; - }, 1800); + }, 2000); }); }); - this.mwse.me.on('scope', () => { - idValue.textContent = this.mwse.me.socketId; - }); + this.mwse.me.on('scope', () => { idValue.textContent = this.mwse.me.socketId; }); bar.appendChild(idCard); - // Durum mesajı + // Durum this._statusEl = document.createElement('span'); this._statusEl.className = 'mwse-studio__status mwse-studio__status--online'; this._statusEl.textContent = 'Bağlı'; @@ -101,6 +102,59 @@ export default class Studio { this._el.insertBefore(bar, this._el.firstChild); } + // ── Gelen istek bildirim alanı ──────────────────────────────────────────── + + _buildNotifArea() { + this._notifArea = document.createElement('div'); + this._notifArea.className = 'mwse-notif-area'; + // Araç çubuğundan hemen sonra + const toolbar = this._el.querySelector('.mwse-studio__toolbar'); + toolbar.insertAdjacentElement('afterend', this._notifArea); + } + + _showPairRequest(peer) { + const bar = document.createElement('div'); + bar.className = 'mwse-notif-bar'; + + const msg = document.createElement('div'); + msg.className = 'mwse-notif-bar__msg'; + msg.innerHTML = `` + + ` ${peer.socketId} bağlanmak istiyor`; + + const actions = document.createElement('div'); + actions.className = 'mwse-notif-bar__actions'; + + const rejectBtn = document.createElement('button'); + rejectBtn.className = 'mwse-btn mwse-btn--danger'; + rejectBtn.textContent = 'Reddet'; + rejectBtn.addEventListener('click', async () => { + await peer.rejectPair().catch(() => {}); + bar.remove(); + this._setStatus('', `${peer.socketId.slice(-8)} reddedildi`); + }); + + const acceptBtn = document.createElement('button'); + acceptBtn.className = 'mwse-btn mwse-btn--primary'; + acceptBtn.textContent = 'Kabul Et'; + acceptBtn.addEventListener('click', async () => { + acceptBtn.textContent = '…'; + acceptBtn.disabled = true; + const ok = await peer.acceptPair().catch(() => false); + bar.remove(); + if (ok) { + this._watchIncoming(peer); + this._view.refresh(); + this._setStatus('online', `${peer.socketId.slice(-8)} kabul edildi`); + } else { + this._setStatus('error', 'Eşleşme kurulamadı'); + } + }); + + actions.append(rejectBtn, acceptBtn); + bar.append(msg, actions); + this._notifArea.appendChild(bar); + } + // ── Kök kolon ───────────────────────────────────────────────────────────── _pushRootColumn() { @@ -139,12 +193,12 @@ export default class Studio { this._view.pushColumn('Studio', items, { searchable: false }); } - // ── Eşler ───────────────────────────────────────────────────────────────── + // ── Eşler kolonu ────────────────────────────────────────────────────────── _pushPeersColumn() { const pairs = [...this.mwse.pairs.values()]; const items = pairs.map(peer => ({ - icon: peer.peerConnection ? '◉' : '●', + icon: peer.rtc?.active ? '◉' : '●', label: peer.socketId, meta: () => this._peerMeta(peer), onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); } @@ -153,7 +207,7 @@ export default class Studio { if (!items.length) { items.push({ icon: '—', label: 'Henüz eş yok', - meta: 'Başka bir istemci bağlanınca görünür', hasChildren: false + meta: '"ID ile ara" → eş socket ID\'sini gir', hasChildren: false }); } @@ -168,7 +222,10 @@ export default class Studio { onConfirm: ({ id }) => { if (!id) return; this.mwse.peer(id).requestPair() - .then(() => this._setStatus('', `${id.slice(-8)}'e istek gönderildi`)) + .then(ok => { + if (ok) this._setStatus('', `${id.slice(-8)}'e istek gönderildi — kabul bekleniyor`); + else this._setStatus('error', 'İstek gönderilemedi'); + }) .catch(e => this._setStatus('error', e.message)); } }); @@ -177,7 +234,7 @@ export default class Studio { _peerMeta(peer) { const streams = peer.rtc?._streams?.list() ?? []; - if (streams.length) return `p2p · ${streams.length} akış aktif`; + if (streams.length) return `p2p · ${streams.length} akış`; if (peer.rtc?.active) return `p2p · ${peer.rtc.connectionStatus}`; return 'websocket'; } @@ -186,43 +243,32 @@ export default class Studio { _pushPeerColumn(peer) { const streams = peer.rtc?._streams?.list() ?? []; - const rtcOn = peer.rtc?.active; const items = [ { - icon: '▶', - label: 'Video + Ses', - meta: 'Kamera ve mikrofon', + icon: '▶', label: 'Video + Ses', meta: 'Kamera ve mikrofon', onSelect: () => this._call(peer, 'cam+mic') }, { - icon: '♪', - label: 'Sesli Ara', - meta: 'Yalnızca mikrofon', + icon: '♪', label: 'Sesli Ara', meta: 'Yalnızca mikrofon', onSelect: () => this._call(peer, 'mic') }, { - icon: '⬜', - label: 'Ekran Paylaş', - meta: 'getDisplayMedia', + icon: '⬜', label: 'Ekran Paylaş', meta: 'getDisplayMedia', onSelect: () => this._call(peer, 'screen') }, { - icon: '◎', - label: 'Kamera Seç', + icon: '◎', label: 'Kamera Seç', meta: `${this._devices.cameras.length} kamera`, onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); } }, { - icon: '♪', - label: 'Mikrofon Seç', + icon: '♪', label: 'Mikrofon Seç', meta: `${this._devices.microphones.length} mikrofon`, onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); } }, { - icon: '↗', - label: 'Dosya Gönder', - meta: 'P2P DataChannel', + icon: '↗', label: 'Dosya Gönder', meta: 'P2P DataChannel', hasChildren: false, onSelect: () => this._sendFile(peer) } @@ -237,21 +283,8 @@ export default class Studio { }); } - if (rtcOn) { - items.push({ - icon: '↺', - label: 'WebRTC Yeniden Başlat', - meta: '', - hasChildren: false, - onSelect: () => { peer.rtc.destroy(); this._ensureRTC(peer); this._view.refresh(); } - }); - } - items.push({ - icon: '⊘', - label: 'Eşleşmeyi Bitir', - meta: '', - hasChildren: false, + icon: '⊘', label: 'Eşleşmeyi Bitir', meta: '', hasChildren: false, onSelect: async () => { await peer.endPair().catch(() => {}); this._view.popTo(1); @@ -262,19 +295,19 @@ export default class Studio { this._view.pushColumn(peer.socketId.slice(-12), items, { searchable: false }); } - // ── Odalar ─────────────────────────────────────────────────────────────── + // ── Odalar kolonu ───────────────────────────────────────────────────────── _pushRoomsColumn() { const items = [...this.mwse.rooms.entries()].map(([id, room]) => ({ icon: '#', - label: id, + label: room.config?.name ?? id, meta: () => `${room.peers.size} üye`, - onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room, id); } + onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room); } })); if (!items.length) { items.push({ - icon: '—', label: 'Oda yok', meta: 'Aşağıdan oluştur', hasChildren: false + icon: '—', label: 'Oda yok', meta: '"Oda Oluştur" ile başla', hasChildren: false }); } @@ -284,13 +317,15 @@ export default class Studio { title: 'Yeni Oda', fields: [ { key: 'name', label: 'Oda adı', placeholder: 'genel' }, - { key: 'pass', label: 'Şifre (opsiyonel)', placeholder: 'boş bırak = şifresiz' } + { key: 'desc', label: 'Açıklama (opsiyonel)', placeholder: 'Genel sohbet odası' }, + { key: 'pass', label: 'Şifre (opsiyonel)', placeholder: 'boş = şifresiz' } ], confirm: 'Oluştur', - onConfirm: async ({ name, pass }) => { + onConfirm: async ({ name, desc, pass }) => { if (!name) return; const room = this.mwse.room({ name, + description: desc || name, joinType: pass ? 'password' : 'free', accessType: 'public', ifexistsJoin: true, @@ -308,7 +343,7 @@ export default class Studio { }); } - _pushRoomMembersColumn(room, roomId) { + _pushRoomMembersColumn(room) { const items = [...room.peers.values()].map(peer => ({ icon: peer.selfSocket ? '★' : '●', label: peer.socketId, @@ -320,17 +355,20 @@ export default class Studio { items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false }); } - this._view.pushColumn(roomId, items); + const roomName = room.config?.name ?? room.roomId; + const col = this._view.pushColumn(roomName, items); + col.addAction('Odadan Çık', 'mwse-btn--danger', async () => { + await room.eject().catch(() => {}); + this._view.popTo(1); + this._pushRoomsColumn(); + }); } - // ── Cihazlar ───────────────────────────────────────────────────────────── + // ── Cihazlar kolonu ─────────────────────────────────────────────────────── _pushDevicesColumn(peer, kind) { - const list = kind === 'audio' - ? this._devices.microphones - : this._devices.cameras; - - const colTitle = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar'; + const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras; + const title = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar'; const items = list.map(dev => ({ icon: kind === 'audio' ? '♪' : '◎', @@ -340,24 +378,19 @@ export default class Studio { const constraints = kind === 'audio' ? { audio: { deviceId: { exact: dev.deviceId } } } : { video: { deviceId: { exact: dev.deviceId } } }; - - const getter = kind === 'audio' + const stream = await (kind === 'audio' ? MediaSources.microphone(constraints) - : MediaSources.camera(constraints); - - const stream = await getter.catch(e => { - this._setStatus('error', e.message); return null; - }); + : MediaSources.camera(constraints) + ).catch(e => { this._setStatus('error', e.message); return null; }); if (!stream) return; if (peer) { this._ensureRTC(peer); - const label = dev.label || dev.deviceId.slice(-6); - peer.rtc.addStream(label, stream); + peer.rtc.addStream(dev.label || dev.deviceId.slice(-6), stream); this._view.popTo(4); this._pushStreamsColumn(peer); } else { - this._previewStream(stream, dev.label || colTitle); + this._previewStream(stream, dev.label || title); } } })); @@ -365,13 +398,11 @@ export default class Studio { if (!items.length) { items.push({ icon: '—', label: 'Cihaz bulunamadı', - meta: 'İzin ver ve sayfayı yenile', hasChildren: false + meta: '"İzin İste" butonunu dene', hasChildren: false }); } - const col = this._view.pushColumn(colTitle, items); - - // İzin isteme butonu + const col = this._view.pushColumn(title, items); col.addAction('İzin İste', '', async () => { await navigator.mediaDevices.getUserMedia( kind === 'audio' ? { audio: true } : { video: true } @@ -382,7 +413,7 @@ export default class Studio { }); } - // ── Akışlar ────────────────────────────────────────────────────────────── + // ── Akışlar kolonu ──────────────────────────────────────────────────────── _pushStreamsColumn(peer) { const srcs = peer.rtc?._streams?.list() ?? []; @@ -419,20 +450,13 @@ export default class Studio { icon: track.enabled ? '⊙' : '○', label: `${track.kind === 'video' ? 'Video' : 'Ses'} ${track.enabled ? 'Sustur' : 'Aç'}`, meta: '', hasChildren: false, - onSelect: () => { - peer.rtc?.setEnabled(label, track.kind, !track.enabled); - this._view.refresh(); - } + onSelect: () => { peer.rtc?.setEnabled(label, track.kind, !track.enabled); this._view.refresh(); } }); } items.push({ icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false, - onSelect: () => { - peer.rtc?.removeStream(label); - this._view.popTo(3); - this._pushStreamsColumn(peer); - } + onSelect: () => { peer.rtc?.removeStream(label); this._view.popTo(3); this._pushStreamsColumn(peer); } }); this._view.pushColumn('Kalite', items, { searchable: false }); @@ -480,7 +504,6 @@ export default class Studio { peer.rtc.connect({ polite }); } - // Gelen track'leri otomatik çal _watchIncoming(peer) { peer.rtc.on('track', track => { if (track.kind === 'audio') { @@ -489,18 +512,13 @@ export default class Studio { audio.srcObject = new MediaStream([track]); document.body.appendChild(audio); this._remoteMedia.push(audio); - this._setStatus('online', `← ${peer.socketId.slice(-8)} ses gönderdi`); - } else { - this._setStatus('online', `← ${peer.socketId.slice(-8)} video gönderdi`); } + this._setStatus('online', `← ${peer.socketId.slice(-8)} ${track.kind} gönderdi`); }); } - // Cihaz test önizlemesi (floating video) _previewStream(stream, label) { - const old = document.getElementById('mwse-preview'); - if (old) old.remove(); - + document.getElementById('mwse-preview')?.remove(); const wrap = document.createElement('div'); wrap.id = 'mwse-preview'; Object.assign(wrap.style, { @@ -508,41 +526,93 @@ export default class Studio { background: '#111', border: '1px solid #333', borderRadius: '6px', overflow: 'hidden', zIndex: '9000' }); - const bar = document.createElement('div'); Object.assign(bar.style, { padding: '4px 8px', fontSize: '11px', color: '#aaa', background: '#1a1a1a', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const closeBtn = document.createElement('span'); - closeBtn.textContent = '✕'; - closeBtn.style.cursor = 'pointer'; - closeBtn.addEventListener('click', () => { - stream.getTracks().forEach(t => t.stop()); - wrap.remove(); - }); + closeBtn.textContent = '✕'; closeBtn.style.cursor = 'pointer'; + closeBtn.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); }); bar.append(document.createTextNode(label), closeBtn); - const video = document.createElement('video'); video.autoplay = true; video.muted = true; video.playsInline = true; video.srcObject = stream; video.style.cssText = 'width:100%;display:block;background:#000'; - wrap.append(bar, video); document.body.appendChild(wrap); } - // ── Cihaz yükleme ───────────────────────────────────────────────────────── + // ── Modal ───────────────────────────────────────────────────────────────── - async _loadDevices() { - try { - this._devices = await MediaSources.devices(); - } catch (_) { - this._devices = { cameras: [], microphones: [] }; + _showModal({ title, fields = [], confirm = 'Tamam', onConfirm }) { + const overlay = document.createElement('div'); + overlay.className = 'mwse-modal-overlay'; + + const modal = document.createElement('div'); + modal.className = 'mwse-modal'; + + const header = document.createElement('div'); + header.className = 'mwse-modal__header'; + const titleEl = document.createElement('span'); + titleEl.textContent = title; + const closeX = document.createElement('span'); + closeX.className = 'mwse-modal__close'; + closeX.textContent = '✕'; + closeX.addEventListener('click', () => overlay.remove()); + header.append(titleEl, closeX); + + const body = document.createElement('div'); + body.className = 'mwse-modal__body'; + const inputs = {}; + + for (const f of fields) { + const wrap = document.createElement('div'); + wrap.className = 'mwse-modal__field'; + const lbl = document.createElement('label'); + lbl.textContent = f.label; + const inp = document.createElement('input'); + inp.className = 'mwse-modal__input'; + inp.placeholder = f.placeholder ?? ''; + inp.type = f.type ?? 'text'; + inputs[f.key] = inp; + inp.addEventListener('keydown', e => { + if (e.key === 'Enter') confirmBtn.click(); + if (e.key === 'Escape') overlay.remove(); + }); + wrap.append(lbl, inp); + body.appendChild(wrap); } + + const footer = document.createElement('div'); + footer.className = 'mwse-modal__footer'; + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'mwse-btn'; + cancelBtn.textContent = 'İptal'; + cancelBtn.addEventListener('click', () => overlay.remove()); + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'mwse-btn mwse-btn--primary'; + confirmBtn.textContent = confirm; + confirmBtn.addEventListener('click', () => { + const values = {}; + for (const [k, el] of Object.entries(inputs)) values[k] = el.value.trim(); + overlay.remove(); + onConfirm(values); + }); + footer.append(cancelBtn, confirmBtn); + modal.append(header, body, footer); + overlay.appendChild(modal); + document.body.appendChild(overlay); + overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); + setTimeout(() => Object.values(inputs)[0]?.focus(), 50); } - // ── Durum çubuğu ───────────────────────────────────────────────────────── + // ── Yardımcılar ────────────────────────────────────────────────────────── + + async _loadDevices() { + try { this._devices = await MediaSources.devices(); } + catch (_) { this._devices = { cameras: [], microphones: [] }; } + } _setStatus(cls, text) { if (!this._statusEl) return; @@ -553,91 +623,10 @@ export default class Studio { this._statusEl.textContent = text; } - // ── Modal ───────────────────────────────────────────────────────────────── - // Kullanım: this._showModal({ title, fields:[{key,label,placeholder}], confirm, onConfirm }) - - _showModal({ title, fields = [], confirm = 'Tamam', onConfirm }) { - const overlay = document.createElement('div'); - overlay.className = 'mwse-modal-overlay'; - - const modal = document.createElement('div'); - modal.className = 'mwse-modal'; - - // Başlık - const header = document.createElement('div'); - header.className = 'mwse-modal__header'; - const titleEl = document.createElement('span'); - titleEl.textContent = title; - const closeBtn = document.createElement('span'); - closeBtn.className = 'mwse-modal__close'; - closeBtn.textContent = '✕'; - closeBtn.addEventListener('click', () => overlay.remove()); - header.append(titleEl, closeBtn); - - // Gövde — alanlar - const body = document.createElement('div'); - body.className = 'mwse-modal__body'; - const inputs = {}; - - for (const f of fields) { - const wrap = document.createElement('div'); - wrap.className = 'mwse-modal__field'; - - const lbl = document.createElement('label'); - lbl.textContent = f.label; - - const inp = document.createElement('input'); - inp.className = 'mwse-modal__input'; - inp.placeholder = f.placeholder ?? ''; - inp.type = f.type ?? 'text'; - inputs[f.key] = inp; - - inp.addEventListener('keydown', e => { - if (e.key === 'Enter') confirmBtn.click(); - if (e.key === 'Escape') overlay.remove(); - }); - - wrap.append(lbl, inp); - body.appendChild(wrap); - } - - // Alt butonlar - const footer = document.createElement('div'); - footer.className = 'mwse-modal__footer'; - - const cancelBtn = document.createElement('button'); - cancelBtn.className = 'mwse-btn'; - cancelBtn.textContent = 'İptal'; - cancelBtn.addEventListener('click', () => overlay.remove()); - - const confirmBtn = document.createElement('button'); - confirmBtn.className = 'mwse-btn mwse-btn--primary'; - confirmBtn.textContent = confirm; - confirmBtn.addEventListener('click', () => { - const values = {}; - for (const [k, el] of Object.entries(inputs)) values[k] = el.value.trim(); - overlay.remove(); - onConfirm(values); - }); - - footer.append(cancelBtn, confirmBtn); - modal.append(header, body, footer); - overlay.appendChild(modal); - document.body.appendChild(overlay); - - // Arka plana tıkla → kapat - overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); - - // İlk alana odaklan - const first = Object.values(inputs)[0]; - if (first) setTimeout(() => first.focus(), 50); - } - _injectStyle() { if (this._styleOK) return; const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = '/studio/style.css'; + link.rel = 'stylesheet'; link.href = '/studio/style.css'; document.head.appendChild(link); this._styleOK = true; } diff --git a/public/studio/style.css b/public/studio/style.css index 1667d4c..25bffca 100644 --- a/public/studio/style.css +++ b/public/studio/style.css @@ -254,6 +254,44 @@ object-fit: contain; } +/* ---- Gelen istek bildirim alanı ---- */ +.mwse-notif-area { flex-shrink: 0; } + +.mwse-notif-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 14px; + background: #162016; + border-bottom: 1px solid #2a3a2a; +} + +.mwse-notif-bar__msg { + flex: 1; + font-size: 12px; + color: #b8d4b8; +} +.mwse-notif-bar__msg code { + font-family: 'Consolas', monospace; + font-size: 11px; + background: #1e2e1e; + padding: 1px 4px; + border-radius: 3px; + color: #90c090; +} + +.mwse-notif-bar__dot { color: #4caf50; margin-right: 4px; } + +.mwse-notif-bar__actions { + display: flex; + gap: 6px; + flex-shrink: 0; +} +.mwse-notif-bar__actions .mwse-btn { + padding: 4px 14px; + font-size: 11px; +} + /* ---- Benim ID kartı ---- */ .mwse-id-card { display: flex; diff --git a/sdk/Peer.js b/sdk/Peer.js index 662f0f0..ce69470 100644 --- a/sdk/Peer.js +++ b/sdk/Peer.js @@ -124,6 +124,7 @@ export default class Peer extends MWSEEventTarget { async endPair() { await this.mwse.EventPooling.request({ type: 'end/pair', to: this.socketId }); + this.mwse.pairs.delete(this.socketId); this.forget(); } @@ -136,6 +137,8 @@ export default class Peer extends MWSEEventTarget { console.error('MWSE: acceptPair failed', status, message); return false; } + // Kabul eden tarafta da pairs haritasını güncelle. + this.mwse.pairs.set(this.socketId, this); return true; } @@ -148,6 +151,7 @@ export default class Peer extends MWSEEventTarget { console.error('MWSE: rejectPair failed', status, message); return false; } + this.mwse.pairs.delete(this.socketId); return true; } diff --git a/sdk/index.js b/sdk/index.js index 713c080..a97e7ad 100644 --- a/sdk/index.js +++ b/sdk/index.js @@ -240,12 +240,15 @@ export default class MWSE { ep.signal('accepted/pair', ({ from, info }) => { const peer = this.peer(from, true); peer.info.info = info; + // İstek gönderen tarafta pairs haritasını doldur. + this.pairs.set(from, peer); peer.emit('accepted/pair', peer); this.peer('me').emit('accepted/pair', peer); }); ep.signal('end/pair', ({ from, info }) => { const peer = this.peer(from, true); + this.pairs.delete(from); peer.emit('end/pair', info); this.peer('me').emit('end/pair', from, info); });