Studio: akış monitörü + Material Icons + saat + başlık büyütme
Akış monitör paneli (sağ kenar, akış varken görünür):
- Gönderiyorum / Geliyor bölümleri ayrı ayrı
- Video tile: <video> önizleme, aspect-ratio 16/9
- Ses tile: mic ikonu + mavi arka plan
- Her tile: etiket, peer IP/ID, ✕ kapat butonu
- Track 'ended' olunca tile otomatik kalkar
- addStream çağrısında localTile eklendi (_call + cihaz seçimi)
- _watchIncoming → remoteTile eklendi
Material Icons Round (@import Google Fonts):
- Tüm item ikonları: people/videocam/mic/screen_share/upload_file/
live_tv/hd/sd/link_off/stop_circle/meeting_room/sensors vb.
- chevron_right ok ikonu
- Column.js: icon string → Material Icons textContent,
HTML içeriyorsa innerHTML (ikonlu butonlar için)
- addAction label: innerHTML → butonlara ikon eklenebilir
- Bildirim banner'ında wifi ikonu
Araç çubuğu:
- Başlık: 12px → 17px, font-weight 700, yükseklik 46px
- ID kartı: "Kimliğim" etiket metni kaldırıldı, sadece IP + UUID + ⎘
- Sağ üst köşe: HH:MM:SS canlı saat (setInterval 1s)
Layout:
- .mwse-studio__main: flex-row → kolonlar sol, panel sağ
- ColumnView artık mainArea'ya mount ediliyor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0c654ae4c8
commit
c1d1ddf383
|
|
@ -65,7 +65,7 @@ export default class Column {
|
||||||
}
|
}
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = `mwse-btn ${className ?? ''}`;
|
btn.className = `mwse-btn ${className ?? ''}`;
|
||||||
btn.textContent = label;
|
btn.innerHTML = label; // HTML destekler (ikonlu etiketler için)
|
||||||
btn.addEventListener('click', onClick);
|
btn.addEventListener('click', onClick);
|
||||||
this._actionsEl.appendChild(btn);
|
this._actionsEl.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,12 @@ export default class Column {
|
||||||
|
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'mwse-item__icon';
|
icon.className = 'mwse-item__icon';
|
||||||
icon.textContent = item.icon ?? '○';
|
// Material Icons: icon adı string ise text, HTML içeriyorsa innerHTML
|
||||||
|
if (item.icon?.includes('<')) {
|
||||||
|
icon.innerHTML = item.icon;
|
||||||
|
} else {
|
||||||
|
icon.textContent = item.icon ?? 'circle';
|
||||||
|
}
|
||||||
row.appendChild(icon);
|
row.appendChild(icon);
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
|
|
@ -120,7 +125,7 @@ export default class Column {
|
||||||
if (item.hasChildren !== false) {
|
if (item.hasChildren !== false) {
|
||||||
const arrow = document.createElement('span');
|
const arrow = document.createElement('span');
|
||||||
arrow.className = 'mwse-item__arrow';
|
arrow.className = 'mwse-item__arrow';
|
||||||
arrow.textContent = '›';
|
arrow.textContent = 'chevron_right';
|
||||||
row.appendChild(arrow);
|
row.appendChild(arrow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
// MWSE Studio — dahili yönetim arayüzü.
|
// MWSE Studio — dahili yönetim arayüzü.
|
||||||
// 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';
|
||||||
|
|
||||||
export default class Studio {
|
export default class Studio {
|
||||||
constructor(mwse, container) {
|
constructor(mwse, container) {
|
||||||
this.mwse = mwse;
|
this.mwse = mwse;
|
||||||
this._el = typeof container === 'string'
|
this._el = typeof container === 'string'
|
||||||
? document.querySelector(container) : container;
|
? document.querySelector(container) : container;
|
||||||
this._view = new ColumnView(this._el);
|
this._view = null; // mount() içinde oluşturulur
|
||||||
this._devices = { cameras: [], microphones: [] };
|
this._devices = { cameras: [], microphones: [] };
|
||||||
this._statusEl = null;
|
this._statusEl = null;
|
||||||
this._notifArea = null;
|
this._notifArea = null;
|
||||||
this._styleOK = false;
|
this._myIPEl = null;
|
||||||
this._remoteMedia = [];
|
this._myUUIDEl = null;
|
||||||
|
this._localGrid = null; // "Gönderiyorum" tile grid'i
|
||||||
|
this._remoteGrid = null; // "Geliyor" tile grid'i
|
||||||
|
this._streamsPanel = null;
|
||||||
|
this._styleOK = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async mount() {
|
async mount() {
|
||||||
|
|
@ -21,30 +24,36 @@ export default class Studio {
|
||||||
this._injectStyle();
|
this._injectStyle();
|
||||||
this._buildToolbar();
|
this._buildToolbar();
|
||||||
this._buildNotifArea();
|
this._buildNotifArea();
|
||||||
|
|
||||||
|
// Ana alan: kolonlar (sol) + akış monitörü (sağ)
|
||||||
|
const mainArea = document.createElement('div');
|
||||||
|
mainArea.className = 'mwse-studio__main';
|
||||||
|
this._el.appendChild(mainArea);
|
||||||
|
|
||||||
|
this._view = new ColumnView(mainArea);
|
||||||
this._view.mount();
|
this._view.mount();
|
||||||
|
|
||||||
|
this._streamsPanel = this._buildStreamsPanel(mainArea);
|
||||||
|
|
||||||
await this._loadDevices();
|
await this._loadDevices();
|
||||||
this._pushRootColumn();
|
this._pushRootColumn();
|
||||||
|
|
||||||
// Scope sonrası sanal IP al ve kendime ata.
|
// Sanal IP al
|
||||||
this.mwse.scope(async () => {
|
this.mwse.scope(async () => {
|
||||||
try {
|
try {
|
||||||
const ip = await this.mwse.virtualPressure.allocAPIPAddress();
|
const ip = await this.mwse.virtualPressure.allocAPIPAddress();
|
||||||
// PeerInfo.set → auth/info → mevcut tüm pairlere yayımlar.
|
|
||||||
this.mwse.me.info.set('ip', ip);
|
this.mwse.me.info.set('ip', ip);
|
||||||
this._updateMyIP(ip);
|
if (this._myIPEl) this._myIPEl.textContent = ip;
|
||||||
|
if (this._myUUIDEl) this._myUUIDEl.textContent = this.mwse.me.socketId.slice(-8);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Gelen eşleme isteği → bildirim banner'ı ──────────────────────
|
// Gelen eşleme isteği → bildirim banner
|
||||||
this.mwse.me.on('request/pair', peer => {
|
this.mwse.me.on('request/pair', peer => this._showPairRequest(peer));
|
||||||
this._showPairRequest(peer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Eşleşme onaylandı (istek gönderen taraf)
|
// Eşleşme onaylandı
|
||||||
this.mwse.me.on('accepted/pair', peer => {
|
this.mwse.me.on('accepted/pair', peer => {
|
||||||
this._watchIncoming(peer);
|
this._watchIncoming(peer);
|
||||||
// Yeni paire kendi IP'mizi paylaş.
|
|
||||||
if (this.mwse.virtualPressure.APIPAddress) {
|
if (this.mwse.virtualPressure.APIPAddress) {
|
||||||
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
|
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
|
||||||
}
|
}
|
||||||
|
|
@ -52,91 +61,201 @@ export default class Studio {
|
||||||
this._setStatus('online', `${this._peerLabel(peer)} eşleşmesi kuruldu`);
|
this._setStatus('online', `${this._peerLabel(peer)} eşleşmesi kuruldu`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pair info değişince (karşı tarafın IP'si gelince) kolonları yenile.
|
|
||||||
this.mwse.me.on('accepted/pair', () => this._view.refresh());
|
|
||||||
this.mwse.on('room', () => this._view.refresh());
|
|
||||||
this.mwse.me.on('end/pair', () => this._view.refresh());
|
this.mwse.me.on('end/pair', () => this._view.refresh());
|
||||||
|
this.mwse.on('room', () => this._view.refresh());
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbar'daki IP/ID kartını güncelle.
|
|
||||||
_updateMyIP(ip) {
|
|
||||||
if (this._myIPEl) this._myIPEl.textContent = ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bir peer için görünen ad: sanal IP varsa onu, yoksa kısa UUID.
|
|
||||||
_peerLabel(peer) {
|
|
||||||
return peer.info?.info?.ip || peer.socketId.slice(-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Araç çubuğu ──────────────────────────────────────────────────────────
|
// ── Araç çubuğu ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_buildToolbar() {
|
_buildToolbar() {
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'mwse-studio__toolbar';
|
bar.className = 'mwse-studio__toolbar';
|
||||||
|
|
||||||
// Logo
|
|
||||||
const logo = document.createElement('span');
|
const logo = document.createElement('span');
|
||||||
logo.className = 'mwse-studio__title';
|
logo.className = 'mwse-studio__title';
|
||||||
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 IP + ID kartı (tıkla → UUID kopyalanır)
|
// ID kartı: sadece IP + kısa UUID + kopyala
|
||||||
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 (paylaşmak için)';
|
idCard.title = 'Socket ID kopyala (bağlantı paylaşımı için)';
|
||||||
|
|
||||||
const idLabel = document.createElement('span');
|
|
||||||
idLabel.className = 'mwse-id-card__label';
|
|
||||||
idLabel.textContent = 'Kimliğim';
|
|
||||||
|
|
||||||
// Sanal IP (önce boş, allocAPIPAddress sonrası dolar)
|
|
||||||
this._myIPEl = document.createElement('span');
|
this._myIPEl = document.createElement('span');
|
||||||
this._myIPEl.className = 'mwse-id-card__ip';
|
this._myIPEl.className = 'mwse-id-card__ip';
|
||||||
this._myIPEl.textContent = '';
|
this._myIPEl.textContent = '●'; // IP gelmeden önce nokta
|
||||||
|
|
||||||
// Kısa UUID (ID paylaşımı için)
|
this._myUUIDEl = document.createElement('span');
|
||||||
const idValue = document.createElement('span');
|
this._myUUIDEl.className = 'mwse-id-card__value';
|
||||||
idValue.className = 'mwse-id-card__value';
|
this._myUUIDEl.textContent = '…';
|
||||||
idValue.textContent = '…';
|
|
||||||
|
|
||||||
const idCopy = document.createElement('span');
|
const copyIcon = document.createElement('span');
|
||||||
idCopy.className = 'mwse-id-card__copy';
|
copyIcon.className = 'mwse-id-card__copy';
|
||||||
idCopy.textContent = '⎘';
|
copyIcon.textContent = '⎘';
|
||||||
|
|
||||||
idCard.append(idLabel, this._myIPEl, idValue, idCopy);
|
idCard.append(this._myIPEl, this._myUUIDEl, copyIcon);
|
||||||
idCard.addEventListener('click', () => {
|
idCard.addEventListener('click', () => {
|
||||||
const id = this.mwse.me.socketId;
|
const id = this.mwse.me.socketId;
|
||||||
if (!id || id === '…') return;
|
if (!id) return;
|
||||||
navigator.clipboard.writeText(id).then(() => {
|
navigator.clipboard.writeText(id).then(() => {
|
||||||
idCopy.textContent = '✓';
|
copyIcon.textContent = '✓';
|
||||||
idCard.classList.add('mwse-id-card--copied');
|
idCard.classList.add('mwse-id-card--copied');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
idCard.classList.remove('mwse-id-card--copied');
|
idCard.classList.remove('mwse-id-card--copied');
|
||||||
idCopy.textContent = '⎘';
|
copyIcon.textContent = '⎘';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mwse.me.on('scope', () => { idValue.textContent = this.mwse.me.socketId.slice(-8); });
|
|
||||||
bar.appendChild(idCard);
|
bar.appendChild(idCard);
|
||||||
|
|
||||||
// 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.innerHTML = '<span class="mi mi-sm">wifi</span> Bağlı';
|
||||||
bar.appendChild(this._statusEl);
|
bar.appendChild(this._statusEl);
|
||||||
|
|
||||||
|
// Canlı saat
|
||||||
|
const clock = document.createElement('span');
|
||||||
|
clock.className = 'mwse-studio__clock';
|
||||||
|
const tick = () => {
|
||||||
|
const now = new Date();
|
||||||
|
clock.textContent = now.toTimeString().slice(0, 8);
|
||||||
|
};
|
||||||
|
tick(); setInterval(tick, 1000);
|
||||||
|
bar.appendChild(clock);
|
||||||
|
|
||||||
this._el.insertBefore(bar, this._el.firstChild);
|
this._el.insertBefore(bar, this._el.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Akış monitör paneli ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_buildStreamsPanel(parent) {
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'mwse-streams-panel';
|
||||||
|
panel.style.display = 'none'; // akış yokken gizli
|
||||||
|
|
||||||
|
// Gönderiyorum bölümü
|
||||||
|
const outSec = document.createElement('div');
|
||||||
|
outSec.className = 'mwse-streams-section';
|
||||||
|
const outTitle = document.createElement('div');
|
||||||
|
outTitle.className = 'mwse-streams-section__title';
|
||||||
|
outTitle.textContent = '▲ Gönderiyorum';
|
||||||
|
this._localGrid = document.createElement('div');
|
||||||
|
this._localGrid.className = 'mwse-streams-grid';
|
||||||
|
outSec.append(outTitle, this._localGrid);
|
||||||
|
|
||||||
|
// Geliyor bölümü
|
||||||
|
const inSec = document.createElement('div');
|
||||||
|
inSec.className = 'mwse-streams-section';
|
||||||
|
const inTitle = document.createElement('div');
|
||||||
|
inTitle.className = 'mwse-streams-section__title';
|
||||||
|
inTitle.textContent = '▼ Geliyor';
|
||||||
|
this._remoteGrid = document.createElement('div');
|
||||||
|
this._remoteGrid.className = 'mwse-streams-grid';
|
||||||
|
inSec.append(inTitle, this._remoteGrid);
|
||||||
|
|
||||||
|
panel.append(outSec, inSec);
|
||||||
|
parent.appendChild(panel);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yerel (gönderilen) akış tile'ı ekle
|
||||||
|
_addLocalTile(label, stream, peerLabel) {
|
||||||
|
const hasVideo = stream.getVideoTracks().length > 0;
|
||||||
|
const tile = this._makeTile(
|
||||||
|
label,
|
||||||
|
`→ ${peerLabel}`,
|
||||||
|
stream,
|
||||||
|
hasVideo,
|
||||||
|
true, // muted (geri besleme yok)
|
||||||
|
() => tile.remove() && this._updatePanelVisibility()
|
||||||
|
);
|
||||||
|
this._localGrid.appendChild(tile);
|
||||||
|
this._updatePanelVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uzak (gelen) track tile'ı ekle
|
||||||
|
_addRemoteTile(track, peerLabel) {
|
||||||
|
const isVideo = track.kind === 'video';
|
||||||
|
const stream = new MediaStream([track]);
|
||||||
|
const tile = this._makeTile(
|
||||||
|
isVideo ? 'Video' : 'Ses',
|
||||||
|
`← ${peerLabel}`,
|
||||||
|
stream,
|
||||||
|
isVideo,
|
||||||
|
!isVideo, // ses tile'ı için muted değil (çalması lazım)
|
||||||
|
() => tile.remove() && this._updatePanelVisibility()
|
||||||
|
);
|
||||||
|
// Ses için görünmez <audio> da ekle
|
||||||
|
if (!isVideo) {
|
||||||
|
const audio = new Audio();
|
||||||
|
audio.autoplay = true;
|
||||||
|
audio.srcObject = stream;
|
||||||
|
document.body.appendChild(audio);
|
||||||
|
}
|
||||||
|
this._remoteGrid.appendChild(tile);
|
||||||
|
this._updatePanelVisibility();
|
||||||
|
|
||||||
|
// Track bitince tile'ı kaldır
|
||||||
|
track.addEventListener('ended', () => {
|
||||||
|
tile.remove();
|
||||||
|
this._updatePanelVisibility();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeTile(label, peerInfo, stream, isVideo, muted, onClose) {
|
||||||
|
const tile = document.createElement('div');
|
||||||
|
tile.className = `mwse-stream-tile${isVideo ? '' : ' mwse-stream-tile--audio'}`;
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.autoplay = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.muted = muted;
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.className = 'mwse-stream-tile__video';
|
||||||
|
tile.appendChild(video);
|
||||||
|
} else {
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'mwse-stream-tile__audio-icon';
|
||||||
|
icon.textContent = '♪';
|
||||||
|
tile.appendChild(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'mwse-stream-tile__info';
|
||||||
|
|
||||||
|
const lbl = document.createElement('span');
|
||||||
|
lbl.className = 'mwse-stream-tile__label';
|
||||||
|
lbl.textContent = label;
|
||||||
|
|
||||||
|
const peer = document.createElement('span');
|
||||||
|
peer.className = 'mwse-stream-tile__peer';
|
||||||
|
peer.textContent = peerInfo;
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('span');
|
||||||
|
closeBtn.className = 'mwse-stream-tile__close';
|
||||||
|
closeBtn.textContent = '✕';
|
||||||
|
closeBtn.title = 'Kapat';
|
||||||
|
closeBtn.addEventListener('click', e => { e.stopPropagation(); onClose(); });
|
||||||
|
|
||||||
|
info.append(lbl, peer, closeBtn);
|
||||||
|
tile.appendChild(info);
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePanelVisibility() {
|
||||||
|
const hasLocal = this._localGrid.childElementCount > 0;
|
||||||
|
const hasRemote = this._remoteGrid.childElementCount > 0;
|
||||||
|
this._streamsPanel.style.display = (hasLocal || hasRemote) ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Gelen istek bildirim alanı ────────────────────────────────────────────
|
// ── Gelen istek bildirim alanı ────────────────────────────────────────────
|
||||||
|
|
||||||
_buildNotifArea() {
|
_buildNotifArea() {
|
||||||
this._notifArea = document.createElement('div');
|
this._notifArea = document.createElement('div');
|
||||||
this._notifArea.className = 'mwse-notif-area';
|
this._notifArea.className = 'mwse-notif-area';
|
||||||
// Araç çubuğundan hemen sonra
|
|
||||||
const toolbar = this._el.querySelector('.mwse-studio__toolbar');
|
const toolbar = this._el.querySelector('.mwse-studio__toolbar');
|
||||||
toolbar.insertAdjacentElement('afterend', this._notifArea);
|
toolbar.insertAdjacentElement('afterend', this._notifArea);
|
||||||
}
|
}
|
||||||
|
|
@ -172,10 +291,12 @@ export default class Studio {
|
||||||
bar.remove();
|
bar.remove();
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this._watchIncoming(peer);
|
this._watchIncoming(peer);
|
||||||
// Kabul eden tarafta Eşler kolonuna yönlendir.
|
if (this.mwse.virtualPressure.APIPAddress) {
|
||||||
|
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
|
||||||
|
}
|
||||||
this._view.popTo(1);
|
this._view.popTo(1);
|
||||||
this._pushPeersColumn();
|
this._pushPeersColumn();
|
||||||
this._setStatus('online', `${peer.socketId.slice(-8)} kabul edildi`);
|
this._setStatus('online', `${this._peerLabel(peer)} kabul edildi`);
|
||||||
} else {
|
} else {
|
||||||
this._setStatus('error', 'Eşleşme kurulamadı');
|
this._setStatus('error', 'Eşleşme kurulamadı');
|
||||||
}
|
}
|
||||||
|
|
@ -191,32 +312,22 @@ export default class Studio {
|
||||||
_pushRootColumn() {
|
_pushRootColumn() {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: '●',
|
icon: 'people', label: 'Eşler',
|
||||||
label: 'Eşler',
|
meta: () => { const n = this.mwse.pairs.size; return n ? `${n} eşleşme` : 'Henüz yok'; },
|
||||||
meta: () => {
|
|
||||||
const n = this.mwse.pairs.size;
|
|
||||||
return n ? `${n} aktif eşleşme` : 'Henüz eş yok';
|
|
||||||
},
|
|
||||||
onSelect: () => { this._view.popTo(1); this._pushPeersColumn(); }
|
onSelect: () => { this._view.popTo(1); this._pushPeersColumn(); }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '#',
|
icon: 'meeting_room', label: 'Odalar',
|
||||||
label: 'Odalar',
|
meta: () => { const n = this.mwse.rooms.size; return n ? `${n} oda` : 'Oda yok'; },
|
||||||
meta: () => {
|
|
||||||
const n = this.mwse.rooms.size;
|
|
||||||
return n ? `${n} oda` : 'Oda yok';
|
|
||||||
},
|
|
||||||
onSelect: () => { this._view.popTo(1); this._pushRoomsColumn(); }
|
onSelect: () => { this._view.popTo(1); this._pushRoomsColumn(); }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '◎',
|
icon: 'videocam', label: 'Kameralar',
|
||||||
label: 'Kameralar',
|
|
||||||
meta: () => `${this._devices.cameras.length} kamera`,
|
meta: () => `${this._devices.cameras.length} kamera`,
|
||||||
onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'video'); }
|
onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'video'); }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '♪',
|
icon: 'mic', label: 'Mikrofonlar',
|
||||||
label: 'Mikrofonlar',
|
|
||||||
meta: () => `${this._devices.microphones.length} mikrofon`,
|
meta: () => `${this._devices.microphones.length} mikrofon`,
|
||||||
onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'audio'); }
|
onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'audio'); }
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +340,7 @@ export default class Studio {
|
||||||
_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.rtc?.active ? '◉' : '●',
|
icon: peer.rtc?.active ? 'sensors' : 'person',
|
||||||
label: this._peerLabel(peer),
|
label: this._peerLabel(peer),
|
||||||
meta: () => this._peerMeta(peer),
|
meta: () => this._peerMeta(peer),
|
||||||
onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); }
|
onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); }
|
||||||
|
|
@ -237,32 +348,33 @@ export default class Studio {
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
items.push({
|
items.push({
|
||||||
icon: '—', label: 'Henüz eş yok',
|
icon: 'person_off', label: 'Henüz eş yok',
|
||||||
meta: '"ID ile ara" → eş socket ID\'sini gir', hasChildren: false
|
meta: '"ID ile ara" butonunu kullan', hasChildren: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const col = this._view.pushColumn('Eşler', items);
|
const col = this._view.pushColumn('Eşler', items);
|
||||||
col.addAction('ID ile ara', '', () => {
|
col.addAction('<span class="mi mi-sm">person_search</span> ID ile ara', '', () => {
|
||||||
this._showModal({
|
this._showModal({
|
||||||
title: 'Eşe bağlan',
|
title: 'Eşe bağlan',
|
||||||
fields: [
|
fields: [{ key: 'id', label: 'Socket ID', placeholder: 'xxxxxxxx-xxxx-…' }],
|
||||||
{ key: 'id', label: 'Socket ID', placeholder: 'xxxxxxxx-xxxx-…' }
|
|
||||||
],
|
|
||||||
confirm: 'Bağlan',
|
confirm: 'Bağlan',
|
||||||
onConfirm: ({ id }) => {
|
onConfirm: ({ id }) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
this.mwse.peer(id).requestPair()
|
this.mwse.peer(id).requestPair()
|
||||||
.then(ok => {
|
.then(ok => ok
|
||||||
if (ok) this._setStatus('', `${id.slice(-8)}'e istek gönderildi — kabul bekleniyor`);
|
? this._setStatus('', `${id.slice(-8)}'e istek gönderildi`)
|
||||||
else this._setStatus('error', 'İstek gönderilemedi');
|
: this._setStatus('error', 'İstek gönderilemedi'))
|
||||||
})
|
|
||||||
.catch(e => this._setStatus('error', e.message));
|
.catch(e => this._setStatus('error', e.message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_peerLabel(peer) {
|
||||||
|
return peer.info?.info?.ip || peer.socketId.slice(-8);
|
||||||
|
}
|
||||||
|
|
||||||
_peerMeta(peer) {
|
_peerMeta(peer) {
|
||||||
const streams = peer.rtc?._streams?.list() ?? [];
|
const streams = peer.rtc?._streams?.list() ?? [];
|
||||||
const id = peer.socketId.slice(-8);
|
const id = peer.socketId.slice(-8);
|
||||||
|
|
@ -277,50 +389,29 @@ export default class Studio {
|
||||||
const streams = peer.rtc?._streams?.list() ?? [];
|
const streams = peer.rtc?._streams?.list() ?? [];
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{ icon: 'videocam', label: 'Video + Ses', meta: 'Kamera ve mikrofon', onSelect: () => this._call(peer, 'cam+mic') },
|
||||||
icon: '▶', label: 'Video + Ses', meta: 'Kamera ve mikrofon',
|
{ icon: 'mic', label: 'Sesli Ara', meta: 'Yalnızca mikrofon', onSelect: () => this._call(peer, 'mic') },
|
||||||
onSelect: () => this._call(peer, 'cam+mic')
|
{ icon: 'screen_share', label: 'Ekran Paylaş', meta: 'getDisplayMedia', onSelect: () => this._call(peer, 'screen') },
|
||||||
},
|
{ icon: 'switch_video', label: 'Kamera Seç', meta: `${this._devices.cameras.length} kamera`,
|
||||||
{
|
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); } },
|
||||||
icon: '♪', label: 'Sesli Ara', meta: 'Yalnızca mikrofon',
|
{ icon: 'settings_voice', label: 'Mikrofon Seç', meta: `${this._devices.microphones.length} mikrofon`,
|
||||||
onSelect: () => this._call(peer, 'mic')
|
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); } },
|
||||||
},
|
{ icon: 'upload_file', label: 'Dosya Gönder', meta: 'P2P DataChannel', hasChildren: false,
|
||||||
{
|
onSelect: () => this._sendFile(peer) }
|
||||||
icon: '⬜', label: 'Ekran Paylaş', meta: 'getDisplayMedia',
|
|
||||||
onSelect: () => this._call(peer, 'screen')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '◎', label: 'Kamera Seç',
|
|
||||||
meta: `${this._devices.cameras.length} kamera`,
|
|
||||||
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
hasChildren: false,
|
|
||||||
onSelect: () => this._sendFile(peer)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (streams.length) {
|
if (streams.length) {
|
||||||
items.push({
|
items.push({
|
||||||
icon: '◉',
|
icon: 'live_tv', label: `Aktif Akışlar (${streams.length})`,
|
||||||
label: `Aktif Akışlar (${streams.length})`,
|
|
||||||
meta: streams.map(s => s.label).join(' · '),
|
meta: streams.map(s => s.label).join(' · '),
|
||||||
onSelect: () => { this._view.popTo(3); this._pushStreamsColumn(peer); }
|
onSelect: () => { this._view.popTo(3); this._pushStreamsColumn(peer); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({ icon: 'link_off', label: 'Eşleşmeyi Bitir', meta: '', hasChildren: false,
|
||||||
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); this._pushPeersColumn();
|
||||||
this._pushPeersColumn();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -330,80 +421,68 @@ export default class Studio {
|
||||||
// ── Odalar kolonu ─────────────────────────────────────────────────────────
|
// ── Odalar kolonu ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_pushRoomsColumn() {
|
_pushRoomsColumn() {
|
||||||
const items = [...this.mwse.rooms.entries()].map(([id, room]) => ({
|
const items = [...this.mwse.rooms.entries()].map(([, room]) => ({
|
||||||
icon: '#',
|
icon: 'meeting_room',
|
||||||
label: room.config?.name ?? id,
|
label: room.config?.name ?? room.roomId,
|
||||||
meta: () => `${room.peers.size} üye`,
|
meta: () => `${room.peers.size} üye`,
|
||||||
onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room); }
|
onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) items.push({ icon: 'block', label: 'Oda yok', meta: '"Oda Oluştur" butonunu kullan', hasChildren: false });
|
||||||
items.push({
|
|
||||||
icon: '—', label: 'Oda yok', meta: '"Oda Oluştur" ile başla', hasChildren: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const col = this._view.pushColumn('Odalar', items);
|
const col = this._view.pushColumn('Odalar', items);
|
||||||
col.addAction('Oda Oluştur', 'mwse-btn--primary', () => {
|
col.addAction('<span class="mi mi-sm">add</span> Oda Oluştur', 'mwse-btn--primary', () => {
|
||||||
this._showModal({
|
this._showModal({
|
||||||
title: 'Yeni Oda',
|
title: 'Yeni Oda',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'name', label: 'Oda adı', placeholder: 'genel' },
|
{ key: 'name', label: 'Oda adı', placeholder: 'genel' },
|
||||||
{ key: 'desc', label: 'Açıklama (opsiyonel)', placeholder: 'Genel sohbet odası' },
|
{ key: 'desc', label: 'Açıklama (opsiyonel)', placeholder: 'Genel sohbet odası' },
|
||||||
{ key: 'pass', label: 'Şifre (opsiyonel)', placeholder: 'boş = şifresiz' }
|
{ key: 'pass', label: 'Şifre (opsiyonel)', placeholder: 'boş = şifresiz' }
|
||||||
],
|
],
|
||||||
confirm: 'Oluştur',
|
confirm: 'Oluştur',
|
||||||
onConfirm: async ({ name, desc, 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,
|
||||||
description: desc || name,
|
joinType: pass ? 'password' : 'free',
|
||||||
joinType: pass ? 'password' : 'free',
|
accessType: 'public', ifexistsJoin: true,
|
||||||
accessType: 'public',
|
notifyActionJoined: true, notifyActionEjected: true, notifyActionInvite: false,
|
||||||
ifexistsJoin: true,
|
|
||||||
notifyActionJoined: true,
|
|
||||||
notifyActionEjected: true,
|
|
||||||
notifyActionInvite: false,
|
|
||||||
...(pass ? { password: pass } : {})
|
...(pass ? { password: pass } : {})
|
||||||
});
|
});
|
||||||
await room.createRoom();
|
await room.createRoom();
|
||||||
this._setStatus('online', `"${name}" odası oluşturuldu`);
|
this._setStatus('online', `"${name}" oluşturuldu`);
|
||||||
this._view.popTo(1);
|
this._view.popTo(1); this._pushRoomsColumn();
|
||||||
this._pushRoomsColumn();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_pushRoomMembersColumn(room) {
|
_pushRoomMembersColumn(room) {
|
||||||
|
const myIP = this.mwse.virtualPressure.APIPAddress;
|
||||||
const items = [...room.peers.values()].map(peer => ({
|
const items = [...room.peers.values()].map(peer => ({
|
||||||
icon: peer.selfSocket ? '★' : '●',
|
icon: peer.selfSocket ? 'star' : 'person',
|
||||||
label: peer.selfSocket ? (this.mwse.virtualPressure.APIPAddress || 'Ben') : this._peerLabel(peer),
|
label: peer.selfSocket ? (myIP || 'Ben') : this._peerLabel(peer),
|
||||||
meta: () => this._peerMeta(peer),
|
meta: () => this._peerMeta(peer),
|
||||||
onSelect: () => { this._view.popTo(3); this._pushPeerColumn(peer); }
|
onSelect: () => { this._view.popTo(3); this._pushPeerColumn(peer); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false });
|
||||||
items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomName = room.config?.name ?? room.roomId;
|
const col = this._view.pushColumn(room.config?.name ?? room.roomId, items);
|
||||||
const col = this._view.pushColumn(roomName, items);
|
col.addAction('<span class="mi mi-sm">exit_to_app</span> Çık', 'mwse-btn--danger', async () => {
|
||||||
col.addAction('Odadan Çık', 'mwse-btn--danger', async () => {
|
|
||||||
await room.eject().catch(() => {});
|
await room.eject().catch(() => {});
|
||||||
this._view.popTo(1);
|
this._view.popTo(1); this._pushRoomsColumn();
|
||||||
this._pushRoomsColumn();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cihazlar kolonu ───────────────────────────────────────────────────────
|
// ── Cihazlar kolonu ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
_pushDevicesColumn(peer, kind) {
|
_pushDevicesColumn(peer, kind) {
|
||||||
const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras;
|
const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras;
|
||||||
const title = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar';
|
const title = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar';
|
||||||
|
|
||||||
const items = list.map(dev => ({
|
const items = list.map(dev => ({
|
||||||
icon: kind === 'audio' ? '♪' : '◎',
|
icon: kind === 'audio' ? 'mic' : 'videocam',
|
||||||
label: dev.label || `Cihaz ${dev.deviceId.slice(-6)}`,
|
label: dev.label || `Cihaz ${dev.deviceId.slice(-6)}`,
|
||||||
meta: dev.deviceId.slice(-8),
|
meta: dev.deviceId.slice(-8),
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
|
|
@ -421,78 +500,64 @@ export default class Studio {
|
||||||
const label = dev.label || dev.deviceId.slice(-6);
|
const label = dev.label || dev.deviceId.slice(-6);
|
||||||
if (peer.rtc._streams?.has(label)) peer.rtc.removeStream(label);
|
if (peer.rtc._streams?.has(label)) peer.rtc.removeStream(label);
|
||||||
peer.rtc.addStream(label, stream);
|
peer.rtc.addStream(label, stream);
|
||||||
this._view.popTo(4);
|
this._addLocalTile(label, stream, this._peerLabel(peer));
|
||||||
this._pushStreamsColumn(peer);
|
this._view.popTo(4); this._pushStreamsColumn(peer);
|
||||||
} else {
|
} else {
|
||||||
this._previewStream(stream, dev.label || title);
|
this._previewStream(stream, dev.label || title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) items.push({ icon: 'perm_media', label: 'Cihaz bulunamadı', meta: '"İzin İste" butonunu kullan', hasChildren: false });
|
||||||
items.push({
|
|
||||||
icon: '—', label: 'Cihaz bulunamadı',
|
|
||||||
meta: '"İzin İste" butonunu dene', hasChildren: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const col = this._view.pushColumn(title, items);
|
const col = this._view.pushColumn(title, items);
|
||||||
col.addAction('İzin İste', '', async () => {
|
col.addAction('<span class="mi mi-sm">lock_open</span> İzin İste', '', async () => {
|
||||||
await navigator.mediaDevices.getUserMedia(
|
await navigator.mediaDevices.getUserMedia(kind === 'audio' ? { audio: true } : { video: true }).catch(() => {});
|
||||||
kind === 'audio' ? { audio: true } : { video: true }
|
|
||||||
).catch(() => {});
|
|
||||||
await this._loadDevices();
|
await this._loadDevices();
|
||||||
this._view.popTo(this._view.depth - 1);
|
this._view.popTo(this._view.depth - 1);
|
||||||
this._pushDevicesColumn(peer, kind);
|
this._pushDevicesColumn(peer, kind);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Akışlar kolonu ────────────────────────────────────────────────────────
|
// ── Akışlar / Kalite kolonları ────────────────────────────────────────────
|
||||||
|
|
||||||
_pushStreamsColumn(peer) {
|
_pushStreamsColumn(peer) {
|
||||||
const srcs = peer.rtc?._streams?.list() ?? [];
|
const srcs = peer.rtc?._streams?.list() ?? [];
|
||||||
const items = srcs.map(src => ({
|
const items = srcs.map(src => ({
|
||||||
icon: src.tracks[0]?.kind === 'video' ? '▶' : '♪',
|
icon: src.tracks[0]?.kind === 'video' ? 'live_tv' : 'graphic_eq',
|
||||||
label: src.label,
|
label: src.label,
|
||||||
meta: src.tracks.map(t => `${t.kind}${t.enabled ? '' : ' (sessiz)'}`).join(' + '),
|
meta: src.tracks.map(t => `${t.kind}${t.enabled ? '' : ' (sessiz)'}`).join(' + '),
|
||||||
onSelect: () => { this._view.popTo(4); this._pushQualityColumn(peer, src.label, src); }
|
onSelect: () => { this._view.popTo(4); this._pushQualityColumn(peer, src.label, src); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) items.push({ icon: '—', label: 'Akış yok', meta: '', hasChildren: false });
|
||||||
items.push({ icon: '—', label: 'Akış yok', meta: '', hasChildren: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
this._view.pushColumn('Akışlar', items);
|
this._view.pushColumn('Akışlar', items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Kalite / kontrol ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_pushQualityColumn(peer, label, src) {
|
_pushQualityColumn(peer, label, src) {
|
||||||
const presets = [
|
const presets = [
|
||||||
{ icon: '↑', label: 'Yüksek', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } },
|
{ icon: 'hd', label: 'Yüksek', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } },
|
||||||
{ icon: '—', label: 'Orta', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } },
|
{ icon: 'sd', label: 'Orta', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } },
|
||||||
{ icon: '↓', label: 'Düşük', meta: '480p · 500 Kbps', params: { maxBitrate: 500_000, scaleResolutionDownBy: 2 } },
|
{ icon: 'signal_cellular_1_bar', label: 'Düşük', meta: '480p · 500 Kbps', params: { maxBitrate: 500_000, scaleResolutionDownBy: 2 } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = presets.map(p => ({
|
const items = presets.map(p => ({
|
||||||
icon: p.icon, label: p.label, meta: p.meta, hasChildren: false,
|
icon: p.icon, label: p.label, meta: p.meta, hasChildren: false,
|
||||||
onSelect: () => peer.rtc?.setEncodings(label, 'video', p.params)
|
onSelect: () => peer.rtc?.setEncodings(label, 'video', p.params)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const track of (src.tracks ?? [])) {
|
for (const track of (src.tracks ?? [])) {
|
||||||
items.push({
|
items.push({
|
||||||
icon: track.enabled ? '⊙' : '○',
|
icon: track.enabled
|
||||||
|
? (track.kind === 'video' ? 'videocam_off' : 'mic_off')
|
||||||
|
: (track.kind === 'video' ? 'videocam' : 'mic'),
|
||||||
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: () => { 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({
|
items.push({
|
||||||
icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false,
|
icon: 'stop_circle', 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 });
|
this._view.pushColumn('Kalite', items, { searchable: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,14 +570,12 @@ export default class Studio {
|
||||||
if (type === 'cam+mic') stream = await MediaSources.cameraAndMic();
|
if (type === 'cam+mic') stream = await MediaSources.cameraAndMic();
|
||||||
else if (type === 'mic') stream = await MediaSources.microphone();
|
else if (type === 'mic') stream = await MediaSources.microphone();
|
||||||
else if (type === 'screen') stream = await MediaSources.screen();
|
else if (type === 'screen') stream = await MediaSources.screen();
|
||||||
} catch (e) {
|
} catch (e) { this._setStatus('error', e.message); return; }
|
||||||
this._setStatus('error', e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Aynı label zaten varsa önce durdur (kullanıcı ikinci kez tıkladı).
|
|
||||||
if (peer.rtc._streams?.has(type)) peer.rtc.removeStream(type);
|
if (peer.rtc._streams?.has(type)) peer.rtc.removeStream(type);
|
||||||
peer.rtc.addStream(type, stream);
|
peer.rtc.addStream(type, stream);
|
||||||
this._setStatus('online', `${type} → ${peer.socketId.slice(-8)}`);
|
this._addLocalTile(type, stream, this._peerLabel(peer));
|
||||||
|
this._setStatus('online', `${type} → ${this._peerLabel(peer)}`);
|
||||||
this._view.popTo(3);
|
this._view.popTo(3);
|
||||||
this._pushStreamsColumn(peer);
|
this._pushStreamsColumn(peer);
|
||||||
}
|
}
|
||||||
|
|
@ -536,20 +599,14 @@ export default class Studio {
|
||||||
|
|
||||||
_ensureRTC(peer) {
|
_ensureRTC(peer) {
|
||||||
if (peer.rtc?._pc) return;
|
if (peer.rtc?._pc) return;
|
||||||
const polite = this.mwse.me.socketId < peer.socketId;
|
peer.rtc.connect({ polite: this.mwse.me.socketId < peer.socketId });
|
||||||
peer.rtc.connect({ polite });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gelen track'leri izle → panel'e ekle
|
||||||
_watchIncoming(peer) {
|
_watchIncoming(peer) {
|
||||||
peer.rtc.on('track', track => {
|
peer.rtc.on('track', track => {
|
||||||
if (track.kind === 'audio') {
|
this._addRemoteTile(track, this._peerLabel(peer));
|
||||||
const audio = new Audio();
|
this._setStatus('online', `← ${this._peerLabel(peer)} ${track.kind}`);
|
||||||
audio.autoplay = true;
|
|
||||||
audio.srcObject = new MediaStream([track]);
|
|
||||||
document.body.appendChild(audio);
|
|
||||||
this._remoteMedia.push(audio);
|
|
||||||
}
|
|
||||||
this._setStatus('online', `← ${peer.socketId.slice(-8)} ${track.kind} gönderdi`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,24 +614,18 @@ export default class Studio {
|
||||||
document.getElementById('mwse-preview')?.remove();
|
document.getElementById('mwse-preview')?.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, { position:'fixed', bottom:'12px', right:'12px', width:'220px',
|
||||||
position: 'fixed', bottom: '12px', right: '12px', width: '220px',
|
background:'#111', border:'1px solid #333', borderRadius:'6px', overflow:'hidden', zIndex:'9000' });
|
||||||
background: '#111', border: '1px solid #333', borderRadius: '6px',
|
|
||||||
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 close = document.createElement('span');
|
||||||
});
|
close.textContent = '✕'; close.style.cursor = 'pointer';
|
||||||
const closeBtn = document.createElement('span');
|
close.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); });
|
||||||
closeBtn.textContent = '✕'; closeBtn.style.cursor = 'pointer';
|
bar.append(document.createTextNode(label), close);
|
||||||
closeBtn.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); });
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -584,56 +635,40 @@ export default class Studio {
|
||||||
_showModal({ title, fields = [], confirm = 'Tamam', onConfirm }) {
|
_showModal({ title, fields = [], confirm = 'Tamam', onConfirm }) {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'mwse-modal-overlay';
|
overlay.className = 'mwse-modal-overlay';
|
||||||
|
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'mwse-modal';
|
modal.className = 'mwse-modal';
|
||||||
|
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'mwse-modal__header';
|
header.className = 'mwse-modal__header';
|
||||||
const titleEl = document.createElement('span');
|
const titleEl = document.createElement('span');
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
const closeX = document.createElement('span');
|
const closeX = document.createElement('span');
|
||||||
closeX.className = 'mwse-modal__close';
|
closeX.className = 'mwse-modal__close'; closeX.textContent = '✕';
|
||||||
closeX.textContent = '✕';
|
|
||||||
closeX.addEventListener('click', () => overlay.remove());
|
closeX.addEventListener('click', () => overlay.remove());
|
||||||
header.append(titleEl, closeX);
|
header.append(titleEl, closeX);
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'mwse-modal__body';
|
body.className = 'mwse-modal__body';
|
||||||
const inputs = {};
|
const inputs = {};
|
||||||
|
|
||||||
for (const f of fields) {
|
for (const f of fields) {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'mwse-modal__field';
|
wrap.className = 'mwse-modal__field';
|
||||||
const lbl = document.createElement('label');
|
const lbl = document.createElement('label'); lbl.textContent = f.label;
|
||||||
lbl.textContent = f.label;
|
|
||||||
const inp = document.createElement('input');
|
const inp = document.createElement('input');
|
||||||
inp.className = 'mwse-modal__input';
|
inp.className = 'mwse-modal__input'; inp.placeholder = f.placeholder ?? ''; inp.type = f.type ?? 'text';
|
||||||
inp.placeholder = f.placeholder ?? '';
|
|
||||||
inp.type = f.type ?? 'text';
|
|
||||||
inputs[f.key] = inp;
|
inputs[f.key] = inp;
|
||||||
inp.addEventListener('keydown', e => {
|
inp.addEventListener('keydown', e => { if (e.key==='Enter') confirmBtn.click(); if (e.key==='Escape') overlay.remove(); });
|
||||||
if (e.key === 'Enter') confirmBtn.click();
|
wrap.append(lbl, inp); body.appendChild(wrap);
|
||||||
if (e.key === 'Escape') overlay.remove();
|
|
||||||
});
|
|
||||||
wrap.append(lbl, inp);
|
|
||||||
body.appendChild(wrap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const footer = document.createElement('div');
|
const footer = document.createElement('div');
|
||||||
footer.className = 'mwse-modal__footer';
|
footer.className = 'mwse-modal__footer';
|
||||||
const cancelBtn = document.createElement('button');
|
const cancelBtn = document.createElement('button');
|
||||||
cancelBtn.className = 'mwse-btn';
|
cancelBtn.className = 'mwse-btn'; cancelBtn.textContent = 'İptal';
|
||||||
cancelBtn.textContent = 'İptal';
|
|
||||||
cancelBtn.addEventListener('click', () => overlay.remove());
|
cancelBtn.addEventListener('click', () => overlay.remove());
|
||||||
const confirmBtn = document.createElement('button');
|
const confirmBtn = document.createElement('button');
|
||||||
confirmBtn.className = 'mwse-btn mwse-btn--primary';
|
confirmBtn.className = 'mwse-btn mwse-btn--primary'; confirmBtn.textContent = confirm;
|
||||||
confirmBtn.textContent = confirm;
|
|
||||||
confirmBtn.addEventListener('click', () => {
|
confirmBtn.addEventListener('click', () => {
|
||||||
const values = {};
|
const values = {};
|
||||||
for (const [k, el] of Object.entries(inputs)) values[k] = el.value.trim();
|
for (const [k, el] of Object.entries(inputs)) values[k] = el.value.trim();
|
||||||
overlay.remove();
|
overlay.remove(); onConfirm(values);
|
||||||
onConfirm(values);
|
|
||||||
});
|
});
|
||||||
footer.append(cancelBtn, confirmBtn);
|
footer.append(cancelBtn, confirmBtn);
|
||||||
modal.append(header, body, footer);
|
modal.append(header, body, footer);
|
||||||
|
|
@ -652,10 +687,7 @@ export default class Studio {
|
||||||
|
|
||||||
_setStatus(cls, text) {
|
_setStatus(cls, text) {
|
||||||
if (!this._statusEl) return;
|
if (!this._statusEl) return;
|
||||||
this._statusEl.className = [
|
this._statusEl.className = ['mwse-studio__status', cls ? `mwse-studio__status--${cls}` : ''].join(' ').trim();
|
||||||
'mwse-studio__status',
|
|
||||||
cls ? `mwse-studio__status--${cls}` : ''
|
|
||||||
].join(' ').trim();
|
|
||||||
this._statusEl.textContent = text;
|
this._statusEl.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +695,6 @@ export default class Studio {
|
||||||
if (this._styleOK) return;
|
if (this._styleOK) return;
|
||||||
const link = document.createElement('link');
|
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);
|
document.head.appendChild(link); this._styleOK = true;
|
||||||
this._styleOK = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
/* MWSE Studio — desktop-first Miller-column UI */
|
/* MWSE Studio — desktop-first Miller-column UI */
|
||||||
|
@import url('https://fonts.googleapis.com/icon?family=Material+Icons+Round');
|
||||||
|
|
||||||
.mwse-studio {
|
.mwse-studio {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -13,35 +14,70 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top toolbar */
|
/* Material icon helper: inline ikon */
|
||||||
|
.mi {
|
||||||
|
font-family: 'Material Icons Round';
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.mi-sm { font-size: 14px; }
|
||||||
|
.mi-lg { font-size: 20px; }
|
||||||
|
|
||||||
|
/* ── Araç çubuğu ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.mwse-studio__toolbar {
|
.mwse-studio__toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 6px 12px;
|
padding: 0 14px;
|
||||||
background: #111;
|
height: 46px;
|
||||||
border-bottom: 1px solid #333;
|
background: #0d0d0d;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-studio__title {
|
.mwse-studio__title {
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 17px;
|
||||||
letter-spacing: .04em;
|
letter-spacing: .03em;
|
||||||
text-transform: uppercase;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-studio__status {
|
.mwse-studio__status {
|
||||||
margin-left: auto;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-studio__status--online { color: #4caf50; }
|
.mwse-studio__status--online { color: #4caf50; }
|
||||||
.mwse-studio__status--error { color: #f44336; }
|
.mwse-studio__status--error { color: #f44336; }
|
||||||
|
|
||||||
/* Column scroll area */
|
/* Saat */
|
||||||
|
.mwse-studio__clock {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ana alan (kolonlar + akış paneli yan yana) ──────────────────────── */
|
||||||
|
|
||||||
|
.mwse-studio__main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Kolon kaydırma alanı ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
.mwse-studio__columns {
|
.mwse-studio__columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -51,12 +87,96 @@
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #444 #1a1a1a;
|
scrollbar-color: #444 #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-studio__columns::-webkit-scrollbar { height: 4px; }
|
.mwse-studio__columns::-webkit-scrollbar { height: 4px; }
|
||||||
.mwse-studio__columns::-webkit-scrollbar-track { background: #1a1a1a; }
|
.mwse-studio__columns::-webkit-scrollbar-track { background: #1a1a1a; }
|
||||||
.mwse-studio__columns::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
|
.mwse-studio__columns::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
|
||||||
|
|
||||||
/* Individual column */
|
/* ── Akış monitör paneli ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.mwse-streams-panel {
|
||||||
|
width: 260px;
|
||||||
|
min-width: 260px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #141414;
|
||||||
|
border-left: 1px solid #2a2a2a;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #333 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-streams-section {
|
||||||
|
padding: 10px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-streams-section__title {
|
||||||
|
padding: 0 12px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #555;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-streams-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tek akış tile'ı */
|
||||||
|
.mwse-stream-tile {
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-stream-tile__video {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
display: block;
|
||||||
|
background: #000;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ses tile'ı (video yok) */
|
||||||
|
.mwse-stream-tile--audio .mwse-stream-tile__audio-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 60px;
|
||||||
|
font-family: 'Material Icons Round';
|
||||||
|
font-size: 28px;
|
||||||
|
color: #0078d4;
|
||||||
|
background: #0d1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-stream-tile__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0,0,0,.5);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwse-stream-tile__label { color: #ccc; font-weight: 600; flex: 1; }
|
||||||
|
.mwse-stream-tile__peer { color: #555; }
|
||||||
|
.mwse-stream-tile__close {
|
||||||
|
cursor: pointer; color: #444; font-size: 12px;
|
||||||
|
padding: 1px 3px; border-radius: 2px;
|
||||||
|
}
|
||||||
|
.mwse-stream-tile__close:hover { background: #333; color: #ccc; }
|
||||||
|
|
||||||
|
/* ── Tek kolon ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.mwse-col {
|
.mwse-col {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
max-width: 260px;
|
max-width: 260px;
|
||||||
|
|
@ -66,21 +186,22 @@
|
||||||
border-right: 1px solid #2e2e2e;
|
border-right: 1px solid #2e2e2e;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-col--active { background: #222; }
|
.mwse-col--active { background: #222; }
|
||||||
|
|
||||||
.mwse-col__header {
|
.mwse-col__header {
|
||||||
padding: 8px 12px 6px;
|
padding: 8px 12px 6px;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
letter-spacing: .06em;
|
letter-spacing: .08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #777;
|
color: #666;
|
||||||
border-bottom: 1px solid #2a2a2a;
|
border-bottom: 1px solid #2a2a2a;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search box */
|
|
||||||
.mwse-col__search {
|
.mwse-col__search {
|
||||||
margin: 6px 8px;
|
margin: 6px 8px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
|
@ -92,10 +213,8 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-col__search:focus { border-color: #0078d4; }
|
.mwse-col__search:focus { border-color: #0078d4; }
|
||||||
|
|
||||||
/* Item list */
|
|
||||||
.mwse-col__list {
|
.mwse-col__list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -103,22 +222,20 @@
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #333 transparent;
|
scrollbar-color: #333 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-col__list::-webkit-scrollbar { width: 4px; }
|
.mwse-col__list::-webkit-scrollbar { width: 4px; }
|
||||||
.mwse-col__list::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
.mwse-col__list::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
||||||
|
|
||||||
/* Single item */
|
/* ── Liste öğesi ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.mwse-item {
|
.mwse-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0;
|
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
transition: background 80ms;
|
transition: background 70ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item:hover { background: #2a2a2a; }
|
.mwse-item:hover { background: #2a2a2a; }
|
||||||
.mwse-item--active {
|
.mwse-item--active {
|
||||||
background: #0d3a5a !important;
|
background: #0d3a5a !important;
|
||||||
|
|
@ -126,13 +243,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item__icon {
|
.mwse-item__icon {
|
||||||
font-size: 12px;
|
font-family: 'Material Icons Round';
|
||||||
|
font-size: 18px;
|
||||||
color: #555;
|
color: #555;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 14px;
|
width: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item--active .mwse-item__icon { color: #60cdff; }
|
.mwse-item--active .mwse-item__icon { color: #60cdff; }
|
||||||
|
|
||||||
.mwse-item__body { flex: 1; min-width: 0; }
|
.mwse-item__body { flex: 1; min-width: 0; }
|
||||||
|
|
@ -144,117 +262,61 @@
|
||||||
color: #d0d0d0;
|
color: #d0d0d0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item--active .mwse-item__label { color: #fff; }
|
.mwse-item--active .mwse-item__label { color: #fff; }
|
||||||
|
|
||||||
.mwse-item__meta {
|
.mwse-item__meta {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #666;
|
color: #555;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item--active .mwse-item__meta { color: #4d9fce; }
|
.mwse-item--active .mwse-item__meta { color: #4d9fce; }
|
||||||
|
|
||||||
.mwse-item__arrow {
|
.mwse-item__arrow {
|
||||||
font-size: 10px;
|
font-family: 'Material Icons Round';
|
||||||
color: #444;
|
font-size: 14px;
|
||||||
|
color: #3a3a3a;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-item--active .mwse-item__arrow { color: #60cdff; }
|
.mwse-item--active .mwse-item__arrow { color: #60cdff; }
|
||||||
|
|
||||||
/* Status badges */
|
/* ── Alt buton alanı ─────────────────────────────────────────────────── */
|
||||||
.mwse-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1px 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: .03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-badge--live { background: #d32f2f; color: #fff; }
|
|
||||||
.mwse-badge--ok { background: #2e7d32; color: #fff; }
|
|
||||||
.mwse-badge--ws { background: #333; color: #aaa; }
|
|
||||||
.mwse-badge--p2p { background: #1565c0; color: #fff; }
|
|
||||||
|
|
||||||
/* Progress bar (file transfer) */
|
|
||||||
.mwse-progress {
|
|
||||||
margin: 8px 12px;
|
|
||||||
height: 4px;
|
|
||||||
background: #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.mwse-progress__bar {
|
|
||||||
height: 100%;
|
|
||||||
background: #0078d4;
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: width 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action buttons inside columns */
|
|
||||||
.mwse-col__actions {
|
.mwse-col__actions {
|
||||||
padding: 8px 12px;
|
padding: 8px 10px;
|
||||||
border-top: 1px solid #2a2a2a;
|
border-top: 1px solid #2a2a2a;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-btn {
|
.mwse-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
background: #2a2a2a;
|
background: #252525;
|
||||||
border: 1px solid #3a3a3a;
|
border: 1px solid #333;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #ccc;
|
color: #bbb;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
.mwse-btn:hover { background: #333; color: #fff; }
|
|
||||||
.mwse-btn--primary { background: #0d47a1; border-color: #1565c0; color: #fff; }
|
|
||||||
.mwse-btn--primary:hover { background: #1565c0; }
|
|
||||||
.mwse-btn--danger { background: #4a1a1a; border-color: #7b2020; color: #f88; }
|
|
||||||
.mwse-btn--danger:hover { background: #7b2020; }
|
|
||||||
|
|
||||||
/* Slider for quality controls */
|
|
||||||
.mwse-slider-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: center;
|
||||||
padding: 4px 12px;
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.mwse-btn:hover { background: #2e2e2e; color: #fff; border-color: #444; }
|
||||||
|
.mwse-btn--primary { background: #0d47a1; border-color: #1565c0; color: #fff; }
|
||||||
|
.mwse-btn--primary:hover { background: #1565c0; }
|
||||||
|
.mwse-btn--danger { background: #3a1010; border-color: #6a2020; color: #f99; }
|
||||||
|
.mwse-btn--danger:hover { background: #6a2020; }
|
||||||
|
|
||||||
.mwse-slider-row label {
|
/* ── Gelen istek bildirimi ───────────────────────────────────────────── */
|
||||||
font-size: 11px;
|
|
||||||
color: #888;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-slider-row input[type=range] {
|
|
||||||
flex: 1;
|
|
||||||
accent-color: #0078d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-slider-row span {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #bbb;
|
|
||||||
width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video preview thumbnail */
|
|
||||||
.mwse-preview {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
background: #111;
|
|
||||||
border-top: 1px solid #2a2a2a;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Gelen istek bildirim alanı ---- */
|
|
||||||
.mwse-notif-area { flex-shrink: 0; }
|
.mwse-notif-area { flex-shrink: 0; }
|
||||||
|
|
||||||
.mwse-notif-bar {
|
.mwse-notif-bar {
|
||||||
|
|
@ -262,178 +324,70 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
background: #162016;
|
background: #0e1f0e;
|
||||||
border-bottom: 1px solid #2a3a2a;
|
border-bottom: 1px solid #1e3a1e;
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-notif-bar__msg {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #b8d4b8;
|
|
||||||
}
|
}
|
||||||
|
.mwse-notif-bar__msg { flex: 1; font-size: 12px; color: #a8c8a8; }
|
||||||
.mwse-notif-bar__msg code {
|
.mwse-notif-bar__msg code {
|
||||||
font-family: 'Consolas', monospace;
|
font-family: 'Consolas', monospace; font-size: 11px;
|
||||||
font-size: 11px;
|
background: #152515; padding: 1px 5px; border-radius: 3px; color: #7cac7c;
|
||||||
background: #1e2e1e;
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #90c090;
|
|
||||||
}
|
}
|
||||||
|
.mwse-notif-bar__dot { color: #4caf50; }
|
||||||
|
.mwse-notif-bar__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
||||||
|
.mwse-notif-bar__actions .mwse-btn { padding: 4px 14px; font-size: 11px; }
|
||||||
|
|
||||||
.mwse-notif-bar__dot { color: #4caf50; margin-right: 4px; }
|
/* ── ID kartı ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.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 {
|
.mwse-id-card {
|
||||||
display: flex;
|
display: flex; align-items: center; gap: 6px;
|
||||||
align-items: center;
|
padding: 4px 10px;
|
||||||
gap: 6px;
|
background: #1e1e1e; border: 1px solid #333; border-radius: 5px;
|
||||||
padding: 3px 8px;
|
cursor: pointer; transition: border-color 120ms;
|
||||||
background: #252525;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 150ms;
|
|
||||||
max-width: 260px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.mwse-id-card:hover { border-color: #0078d4; }
|
.mwse-id-card:hover { border-color: #0078d4; }
|
||||||
|
|
||||||
.mwse-id-card__label {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #666;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .05em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sanal IP (büyük, belirgin) */
|
|
||||||
.mwse-id-card__ip {
|
.mwse-id-card__ip {
|
||||||
font-family: 'Consolas', 'Menlo', monospace;
|
font-family: 'Consolas', monospace; font-size: 13px;
|
||||||
font-size: 13px;
|
font-weight: 700; color: #60cdff; white-space: nowrap;
|
||||||
font-weight: 600;
|
|
||||||
color: #60cdff;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.mwse-id-card__ip:empty { display: none; }
|
.mwse-id-card__ip:empty + .mwse-id-card__value { margin-left: 0; }
|
||||||
|
|
||||||
/* UUID (küçük, soluk) */
|
|
||||||
.mwse-id-card__value {
|
.mwse-id-card__value {
|
||||||
font-family: 'Consolas', 'Menlo', monospace;
|
font-family: 'Consolas', monospace; font-size: 10px;
|
||||||
font-size: 10px;
|
color: #555; white-space: nowrap;
|
||||||
color: #666;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-id-card__copy {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #555;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: color 150ms;
|
|
||||||
}
|
}
|
||||||
|
.mwse-id-card__copy { font-size: 14px; color: #444; }
|
||||||
.mwse-id-card:hover .mwse-id-card__copy { color: #0078d4; }
|
.mwse-id-card:hover .mwse-id-card__copy { color: #0078d4; }
|
||||||
|
.mwse-id-card--copied { border-color: #4caf50 !important; }
|
||||||
|
.mwse-id-card--copied .mwse-id-card__ip,
|
||||||
|
.mwse-id-card--copied .mwse-id-card__copy { color: #4caf50; }
|
||||||
|
|
||||||
.mwse-id-card--copied {
|
/* ── Modal ───────────────────────────────────────────────────────────── */
|
||||||
border-color: #4caf50 !important;
|
|
||||||
}
|
|
||||||
.mwse-id-card--copied .mwse-id-card__value { color: #4caf50; }
|
|
||||||
.mwse-id-card--copied .mwse-id-card__copy { color: #4caf50; }
|
|
||||||
|
|
||||||
/* ---- Modal ---- */
|
|
||||||
.mwse-modal-overlay {
|
.mwse-modal-overlay {
|
||||||
position: fixed;
|
position: fixed; inset: 0; background: rgba(0,0,0,.7);
|
||||||
inset: 0;
|
display: flex; align-items: center; justify-content: center; z-index: 9999;
|
||||||
background: rgba(0,0,0,.65);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-modal {
|
.mwse-modal {
|
||||||
background: #1e1e1e;
|
background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 6px;
|
||||||
border: 1px solid #3a3a3a;
|
width: 380px; max-width: calc(100vw - 32px);
|
||||||
border-radius: 6px;
|
box-shadow: 0 12px 40px rgba(0,0,0,.6);
|
||||||
width: 360px;
|
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mwse-modal__header {
|
.mwse-modal__header {
|
||||||
padding: 12px 16px 10px;
|
padding: 12px 16px 10px; border-bottom: 1px solid #2a2a2a;
|
||||||
border-bottom: 1px solid #2a2a2a;
|
font-weight: 600; font-size: 13px; color: #e0e0e0;
|
||||||
font-weight: 600;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
font-size: 13px;
|
|
||||||
color: #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-modal__close {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #555;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
.mwse-modal__close { cursor: pointer; color: #555; font-size: 16px; padding: 2px 4px; border-radius: 3px; }
|
||||||
.mwse-modal__close:hover { background: #2a2a2a; color: #ccc; }
|
.mwse-modal__close:hover { background: #2a2a2a; color: #ccc; }
|
||||||
|
.mwse-modal__body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
||||||
.mwse-modal__body {
|
.mwse-modal__field { display: flex; flex-direction: column; gap: 4px; }
|
||||||
padding: 16px;
|
.mwse-modal__field label { font-size: 11px; color: #777; }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-modal__field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-modal__field label {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #888;
|
|
||||||
letter-spacing: .03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-modal__input {
|
.mwse-modal__input {
|
||||||
width: 100%;
|
width: 100%; padding: 7px 10px; background: #2a2a2a;
|
||||||
padding: 7px 10px;
|
border: 1px solid #3a3a3a; border-radius: 4px; color: #d4d4d4;
|
||||||
background: #2a2a2a;
|
font-size: 13px; outline: none; box-sizing: border-box;
|
||||||
border: 1px solid #3a3a3a;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #d4d4d4;
|
|
||||||
font-size: 13px;
|
|
||||||
outline: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
.mwse-modal__input:focus { border-color: #0078d4; }
|
.mwse-modal__input:focus { border-color: #0078d4; }
|
||||||
|
.mwse-modal__footer { padding: 10px 16px 14px; display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
.mwse-modal__footer {
|
.mwse-modal__footer .mwse-btn { flex: none; padding: 6px 20px; font-size: 12px; }
|
||||||
padding: 10px 16px 14px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mwse-modal__footer .mwse-btn {
|
|
||||||
flex: none;
|
|
||||||
padding: 6px 18px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue