Studio: eşleşme akışı tam çalışır hale getirildi

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 <noreply@anthropic.com>
This commit is contained in:
abdussamedulutas 2026-06-17 13:38:17 +03:00
parent 3bba5af340
commit 3736d78dfe
4 changed files with 233 additions and 199 deletions

View File

@ -1,5 +1,5 @@
// MWSE Studio — dahili yönetim arayüzü. // 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 ColumnView from '/studio/ColumnView.js';
import { MediaSources } from '/sdk/webrtc/index.js'; import { MediaSources } from '/sdk/webrtc/index.js';
@ -11,6 +11,7 @@ export default class Studio {
this._view = new ColumnView(this._el); this._view = new ColumnView(this._el);
this._devices = { cameras: [], microphones: [] }; this._devices = { cameras: [], microphones: [] };
this._statusEl = null; this._statusEl = null;
this._notifArea = null;
this._styleOK = false; this._styleOK = false;
this._remoteMedia = []; this._remoteMedia = [];
} }
@ -19,26 +20,28 @@ export default class Studio {
this._el.classList.add('mwse-studio'); this._el.classList.add('mwse-studio');
this._injectStyle(); this._injectStyle();
this._buildToolbar(); this._buildToolbar();
this._buildNotifArea();
this._view.mount(); this._view.mount();
// Cihazları yükle
await this._loadDevices(); await this._loadDevices();
// Kök kolon
this._pushRootColumn(); this._pushRootColumn();
// Gelen eşleme isteği bildirimi // ── Gelen eşleme isteği → bildirim banner'ı ──────────────────────
this.mwse.me.on('request/pair', peer => { 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.mwse.me.on('accepted/pair', peer => {
this._watchIncoming(peer); this._watchIncoming(peer);
this._view.refresh(); 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()); this.mwse.me.on('end/pair', () => this._view.refresh());
// Oda değişimi
this.mwse.on('room', () => this._view.refresh()); this.mwse.on('room', () => this._view.refresh());
return this; return this;
@ -56,14 +59,14 @@ export default class Studio {
logo.innerHTML = 'MWSE <span style="color:#0078d4">Studio</span>'; logo.innerHTML = 'MWSE <span style="color:#0078d4">Studio</span>';
bar.appendChild(logo); bar.appendChild(logo);
// Benim ID kartı — kopyalanabilir // Benim ID kartı
const idCard = document.createElement('div'); const idCard = document.createElement('div');
idCard.className = 'mwse-id-card'; idCard.className = 'mwse-id-card';
idCard.title = 'Socket ID\'yi kopyala'; idCard.title = 'Tıkla → Kopyala';
const idLabel = document.createElement('span'); const idLabel = document.createElement('span');
idLabel.className = 'mwse-id-card__label'; idLabel.className = 'mwse-id-card__label';
idLabel.textContent = 'ID'; idLabel.textContent = 'Kimliğim';
const idValue = document.createElement('span'); const idValue = document.createElement('span');
idValue.className = 'mwse-id-card__value'; idValue.className = 'mwse-id-card__value';
@ -83,16 +86,14 @@ export default class Studio {
setTimeout(() => { setTimeout(() => {
idCard.classList.remove('mwse-id-card--copied'); idCard.classList.remove('mwse-id-card--copied');
idCopy.textContent = '⎘'; idCopy.textContent = '⎘';
}, 1800); }, 2000);
}); });
}); });
this.mwse.me.on('scope', () => { this.mwse.me.on('scope', () => { idValue.textContent = this.mwse.me.socketId; });
idValue.textContent = this.mwse.me.socketId;
});
bar.appendChild(idCard); bar.appendChild(idCard);
// Durum mesajı // Durum
this._statusEl = document.createElement('span'); this._statusEl = document.createElement('span');
this._statusEl.className = 'mwse-studio__status mwse-studio__status--online'; this._statusEl.className = 'mwse-studio__status mwse-studio__status--online';
this._statusEl.textContent = 'Bağlı'; this._statusEl.textContent = 'Bağlı';
@ -101,6 +102,59 @@ export default class Studio {
this._el.insertBefore(bar, this._el.firstChild); 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 = `<span class="mwse-notif-bar__dot">●</span>`
+ ` <code>${peer.socketId}</code> 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 ───────────────────────────────────────────────────────────── // ── Kök kolon ─────────────────────────────────────────────────────────────
_pushRootColumn() { _pushRootColumn() {
@ -139,12 +193,12 @@ export default class Studio {
this._view.pushColumn('Studio', items, { searchable: false }); this._view.pushColumn('Studio', items, { searchable: false });
} }
// ── Eşler ───────────────────────────────────────────────────────────────── // ── Eşler kolonu ──────────────────────────────────────────────────────────
_pushPeersColumn() { _pushPeersColumn() {
const pairs = [...this.mwse.pairs.values()]; const pairs = [...this.mwse.pairs.values()];
const items = pairs.map(peer => ({ const items = pairs.map(peer => ({
icon: peer.peerConnection ? '◉' : '●', icon: peer.rtc?.active ? '◉' : '●',
label: peer.socketId, label: peer.socketId,
meta: () => this._peerMeta(peer), meta: () => this._peerMeta(peer),
onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); } onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); }
@ -153,7 +207,7 @@ export default class Studio {
if (!items.length) { if (!items.length) {
items.push({ items.push({
icon: '—', label: 'Henüz eş yok', 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 }) => { onConfirm: ({ id }) => {
if (!id) return; if (!id) return;
this.mwse.peer(id).requestPair() 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)); .catch(e => this._setStatus('error', e.message));
} }
}); });
@ -177,7 +234,7 @@ export default class Studio {
_peerMeta(peer) { _peerMeta(peer) {
const streams = peer.rtc?._streams?.list() ?? []; 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}`; if (peer.rtc?.active) return `p2p · ${peer.rtc.connectionStatus}`;
return 'websocket'; return 'websocket';
} }
@ -186,43 +243,32 @@ export default class Studio {
_pushPeerColumn(peer) { _pushPeerColumn(peer) {
const streams = peer.rtc?._streams?.list() ?? []; const streams = peer.rtc?._streams?.list() ?? [];
const rtcOn = peer.rtc?.active;
const items = [ const items = [
{ {
icon: '▶', icon: '▶', label: 'Video + Ses', meta: 'Kamera ve mikrofon',
label: 'Video + Ses',
meta: 'Kamera ve mikrofon',
onSelect: () => this._call(peer, 'cam+mic') onSelect: () => this._call(peer, 'cam+mic')
}, },
{ {
icon: '♪', icon: '♪', label: 'Sesli Ara', meta: 'Yalnızca mikrofon',
label: 'Sesli Ara',
meta: 'Yalnızca mikrofon',
onSelect: () => this._call(peer, 'mic') onSelect: () => this._call(peer, 'mic')
}, },
{ {
icon: '⬜', icon: '⬜', label: 'Ekran Paylaş', meta: 'getDisplayMedia',
label: 'Ekran Paylaş',
meta: 'getDisplayMedia',
onSelect: () => this._call(peer, 'screen') onSelect: () => this._call(peer, 'screen')
}, },
{ {
icon: '◎', icon: '◎', label: 'Kamera Seç',
label: 'Kamera Seç',
meta: `${this._devices.cameras.length} kamera`, meta: `${this._devices.cameras.length} kamera`,
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); } onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); }
}, },
{ {
icon: '♪', icon: '♪', label: 'Mikrofon Seç',
label: 'Mikrofon Seç',
meta: `${this._devices.microphones.length} mikrofon`, meta: `${this._devices.microphones.length} mikrofon`,
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); } onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); }
}, },
{ {
icon: '↗', icon: '↗', label: 'Dosya Gönder', meta: 'P2P DataChannel',
label: 'Dosya Gönder',
meta: 'P2P DataChannel',
hasChildren: false, hasChildren: false,
onSelect: () => this._sendFile(peer) onSelect: () => this._sendFile(peer)
} }
@ -237,21 +283,8 @@ export default class Studio {
}); });
} }
if (rtcOn) {
items.push({ items.push({
icon: '↺', icon: '⊘', label: 'Eşleşmeyi Bitir', meta: '', hasChildren: false,
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,
onSelect: async () => { onSelect: async () => {
await peer.endPair().catch(() => {}); await peer.endPair().catch(() => {});
this._view.popTo(1); this._view.popTo(1);
@ -262,19 +295,19 @@ export default class Studio {
this._view.pushColumn(peer.socketId.slice(-12), items, { searchable: false }); this._view.pushColumn(peer.socketId.slice(-12), items, { searchable: false });
} }
// ── Odalar ─────────────────────────────────────────────────────────────── // ── Odalar kolonu ─────────────────────────────────────────────────────────
_pushRoomsColumn() { _pushRoomsColumn() {
const items = [...this.mwse.rooms.entries()].map(([id, room]) => ({ const items = [...this.mwse.rooms.entries()].map(([id, room]) => ({
icon: '#', icon: '#',
label: id, label: room.config?.name ?? id,
meta: () => `${room.peers.size} üye`, 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) { if (!items.length) {
items.push({ 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', title: 'Yeni Oda',
fields: [ fields: [
{ key: 'name', label: 'Oda adı', placeholder: 'genel' }, { 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', confirm: 'Oluştur',
onConfirm: async ({ name, pass }) => { onConfirm: async ({ name, desc, pass }) => {
if (!name) return; if (!name) return;
const room = this.mwse.room({ const room = this.mwse.room({
name, name,
description: desc || name,
joinType: pass ? 'password' : 'free', joinType: pass ? 'password' : 'free',
accessType: 'public', accessType: 'public',
ifexistsJoin: true, ifexistsJoin: true,
@ -308,7 +343,7 @@ export default class Studio {
}); });
} }
_pushRoomMembersColumn(room, roomId) { _pushRoomMembersColumn(room) {
const items = [...room.peers.values()].map(peer => ({ const items = [...room.peers.values()].map(peer => ({
icon: peer.selfSocket ? '★' : '●', icon: peer.selfSocket ? '★' : '●',
label: peer.socketId, label: peer.socketId,
@ -320,17 +355,20 @@ export default class Studio {
items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false }); 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) { _pushDevicesColumn(peer, kind) {
const list = kind === 'audio' const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras;
? this._devices.microphones const title = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar';
: this._devices.cameras;
const colTitle = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar';
const items = list.map(dev => ({ const items = list.map(dev => ({
icon: kind === 'audio' ? '♪' : '◎', icon: kind === 'audio' ? '♪' : '◎',
@ -340,24 +378,19 @@ export default class Studio {
const constraints = kind === 'audio' const constraints = kind === 'audio'
? { audio: { deviceId: { exact: dev.deviceId } } } ? { audio: { deviceId: { exact: dev.deviceId } } }
: { video: { deviceId: { exact: dev.deviceId } } }; : { video: { deviceId: { exact: dev.deviceId } } };
const stream = await (kind === 'audio'
const getter = kind === 'audio'
? MediaSources.microphone(constraints) ? MediaSources.microphone(constraints)
: MediaSources.camera(constraints); : MediaSources.camera(constraints)
).catch(e => { this._setStatus('error', e.message); return null; });
const stream = await getter.catch(e => {
this._setStatus('error', e.message); return null;
});
if (!stream) return; if (!stream) return;
if (peer) { if (peer) {
this._ensureRTC(peer); this._ensureRTC(peer);
const label = dev.label || dev.deviceId.slice(-6); peer.rtc.addStream(dev.label || dev.deviceId.slice(-6), stream);
peer.rtc.addStream(label, stream);
this._view.popTo(4); this._view.popTo(4);
this._pushStreamsColumn(peer); this._pushStreamsColumn(peer);
} else { } else {
this._previewStream(stream, dev.label || colTitle); this._previewStream(stream, dev.label || title);
} }
} }
})); }));
@ -365,13 +398,11 @@ export default class Studio {
if (!items.length) { if (!items.length) {
items.push({ items.push({
icon: '—', label: 'Cihaz bulunamadı', 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); const col = this._view.pushColumn(title, items);
// İzin isteme butonu
col.addAction('İzin İste', '', async () => { col.addAction('İzin İste', '', async () => {
await navigator.mediaDevices.getUserMedia( await navigator.mediaDevices.getUserMedia(
kind === 'audio' ? { audio: true } : { video: true } kind === 'audio' ? { audio: true } : { video: true }
@ -382,7 +413,7 @@ export default class Studio {
}); });
} }
// ── Akışlar ────────────────────────────────────────────────────────────── // ── Akışlar kolonu ────────────────────────────────────────────────────────
_pushStreamsColumn(peer) { _pushStreamsColumn(peer) {
const srcs = peer.rtc?._streams?.list() ?? []; const srcs = peer.rtc?._streams?.list() ?? [];
@ -419,20 +450,13 @@ export default class Studio {
icon: track.enabled ? '⊙' : '○', icon: track.enabled ? '⊙' : '○',
label: `${track.kind === 'video' ? 'Video' : 'Ses'} ${track.enabled ? 'Sustur' : 'Aç'}`, label: `${track.kind === 'video' ? 'Video' : 'Ses'} ${track.enabled ? 'Sustur' : 'Aç'}`,
meta: '', hasChildren: false, meta: '', hasChildren: false,
onSelect: () => { onSelect: () => { peer.rtc?.setEnabled(label, track.kind, !track.enabled); this._view.refresh(); }
peer.rtc?.setEnabled(label, track.kind, !track.enabled);
this._view.refresh();
}
}); });
} }
items.push({ items.push({
icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false, icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false,
onSelect: () => { onSelect: () => { peer.rtc?.removeStream(label); this._view.popTo(3); this._pushStreamsColumn(peer); }
peer.rtc?.removeStream(label);
this._view.popTo(3);
this._pushStreamsColumn(peer);
}
}); });
this._view.pushColumn('Kalite', items, { searchable: false }); this._view.pushColumn('Kalite', items, { searchable: false });
@ -480,7 +504,6 @@ export default class Studio {
peer.rtc.connect({ polite }); peer.rtc.connect({ polite });
} }
// Gelen track'leri otomatik çal
_watchIncoming(peer) { _watchIncoming(peer) {
peer.rtc.on('track', track => { peer.rtc.on('track', track => {
if (track.kind === 'audio') { if (track.kind === 'audio') {
@ -489,18 +512,13 @@ export default class Studio {
audio.srcObject = new MediaStream([track]); audio.srcObject = new MediaStream([track]);
document.body.appendChild(audio); document.body.appendChild(audio);
this._remoteMedia.push(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) { _previewStream(stream, label) {
const old = document.getElementById('mwse-preview'); document.getElementById('mwse-preview')?.remove();
if (old) old.remove();
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.id = 'mwse-preview'; wrap.id = 'mwse-preview';
Object.assign(wrap.style, { Object.assign(wrap.style, {
@ -508,41 +526,93 @@ export default class Studio {
background: '#111', border: '1px solid #333', borderRadius: '6px', background: '#111', border: '1px solid #333', borderRadius: '6px',
overflow: 'hidden', zIndex: '9000' overflow: 'hidden', zIndex: '9000'
}); });
const bar = document.createElement('div'); const bar = document.createElement('div');
Object.assign(bar.style, { Object.assign(bar.style, {
padding: '4px 8px', fontSize: '11px', color: '#aaa', background: '#1a1a1a', padding: '4px 8px', fontSize: '11px', color: '#aaa', background: '#1a1a1a',
display: 'flex', justifyContent: 'space-between', alignItems: 'center' display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}); });
const closeBtn = document.createElement('span'); const closeBtn = document.createElement('span');
closeBtn.textContent = '✕'; closeBtn.textContent = '✕'; closeBtn.style.cursor = 'pointer';
closeBtn.style.cursor = 'pointer'; closeBtn.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); });
closeBtn.addEventListener('click', () => {
stream.getTracks().forEach(t => t.stop());
wrap.remove();
});
bar.append(document.createTextNode(label), closeBtn); bar.append(document.createTextNode(label), closeBtn);
const video = document.createElement('video'); const video = document.createElement('video');
video.autoplay = true; video.muted = true; video.playsInline = true; video.autoplay = true; video.muted = true; video.playsInline = true;
video.srcObject = stream; video.srcObject = stream;
video.style.cssText = 'width:100%;display:block;background:#000'; video.style.cssText = 'width:100%;display:block;background:#000';
wrap.append(bar, video); wrap.append(bar, video);
document.body.appendChild(wrap); document.body.appendChild(wrap);
} }
// ── Cihaz yükleme ───────────────────────────────────────────────────────── // ── Modal ─────────────────────────────────────────────────────────────────
_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);
}
// ── Yardımcılar ──────────────────────────────────────────────────────────
async _loadDevices() { async _loadDevices() {
try { try { this._devices = await MediaSources.devices(); }
this._devices = await MediaSources.devices(); catch (_) { this._devices = { cameras: [], microphones: [] }; }
} catch (_) {
this._devices = { cameras: [], microphones: [] };
} }
}
// ── Durum çubuğu ─────────────────────────────────────────────────────────
_setStatus(cls, text) { _setStatus(cls, text) {
if (!this._statusEl) return; if (!this._statusEl) return;
@ -553,91 +623,10 @@ export default class Studio {
this._statusEl.textContent = text; 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() { _injectStyle() {
if (this._styleOK) return; if (this._styleOK) return;
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet'; link.href = '/studio/style.css';
link.href = '/studio/style.css';
document.head.appendChild(link); document.head.appendChild(link);
this._styleOK = true; this._styleOK = true;
} }

View File

@ -254,6 +254,44 @@
object-fit: contain; 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ı ---- */ /* ---- Benim ID kartı ---- */
.mwse-id-card { .mwse-id-card {
display: flex; display: flex;

View File

@ -124,6 +124,7 @@ export default class Peer extends MWSEEventTarget {
async endPair() { async endPair() {
await this.mwse.EventPooling.request({ type: 'end/pair', to: this.socketId }); await this.mwse.EventPooling.request({ type: 'end/pair', to: this.socketId });
this.mwse.pairs.delete(this.socketId);
this.forget(); this.forget();
} }
@ -136,6 +137,8 @@ export default class Peer extends MWSEEventTarget {
console.error('MWSE: acceptPair failed', status, message); console.error('MWSE: acceptPair failed', status, message);
return false; return false;
} }
// Kabul eden tarafta da pairs haritasını güncelle.
this.mwse.pairs.set(this.socketId, this);
return true; return true;
} }
@ -148,6 +151,7 @@ export default class Peer extends MWSEEventTarget {
console.error('MWSE: rejectPair failed', status, message); console.error('MWSE: rejectPair failed', status, message);
return false; return false;
} }
this.mwse.pairs.delete(this.socketId);
return true; return true;
} }

View File

@ -240,12 +240,15 @@ export default class MWSE {
ep.signal('accepted/pair', ({ from, info }) => { ep.signal('accepted/pair', ({ from, info }) => {
const peer = this.peer(from, true); const peer = this.peer(from, true);
peer.info.info = info; peer.info.info = info;
// İstek gönderen tarafta pairs haritasını doldur.
this.pairs.set(from, peer);
peer.emit('accepted/pair', peer); peer.emit('accepted/pair', peer);
this.peer('me').emit('accepted/pair', peer); this.peer('me').emit('accepted/pair', peer);
}); });
ep.signal('end/pair', ({ from, info }) => { ep.signal('end/pair', ({ from, info }) => {
const peer = this.peer(from, true); const peer = this.peer(from, true);
this.pairs.delete(from);
peer.emit('end/pair', info); peer.emit('end/pair', info);
this.peer('me').emit('end/pair', from, info); this.peer('me').emit('end/pair', from, info);
}); });