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);
});