Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ View your teleprompter on **any device** — phone, tablet, or another computer
- **Enable in Settings → Remote** — Starts a lightweight HTTP + WebSocket server on your Mac.
- **QR code** — Scan the generated QR code from your phone or tablet to open the teleprompter instantly.
- **Real-time sync** — Words highlight, waveform animates, and progress updates in real time over WebSocket.
- **Mirror toggle (per device)** — Tap **Mirror** in the remote view to flip horizontally for teleprompter glass.
- **Mirror URL override** — Append `?mirror=1` (mirrored) or `?mirror=0` (normal), for example `http://YOUR-IP:7373?mirror=1`.
- **No app needed** — Works in any modern browser. No installation required on the remote device.
- **Configurable port** — Default port 7373, adjustable in advanced settings.
- **Fully local** — All traffic stays on your local network. Nothing leaves your Wi-Fi.
Expand Down
57 changes: 56 additions & 1 deletion Textream/Textream/BrowserServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,16 @@ class BrowserServer {
#mic-dot{width:10px;height:10px;border-radius:50%;
background:#facc15;opacity:0;transition:opacity .2s}
#mic-dot.on{opacity:1}
#mirror-btn{height:40px;min-width:40px;padding:0 12px;border-radius:20px;
border:0;background:rgba(255,255,255,.15);color:rgba(255,255,255,.75);
display:flex;align-items:center;justify-content:center;gap:6px;
font-size:13px;font-weight:600;line-height:1;cursor:pointer;
transition:background .2s,color .2s}
#mirror-btn .icon{font-size:14px}
#mirror-btn.on{background:rgba(250,204,21,.22);color:#facc15}

body.mirrored #main,body.mirrored #done{
transform:scaleX(-1);transform-origin:center}

/* Done */
#done{display:none;flex-direction:column;align-items:center;
Expand All @@ -378,6 +388,8 @@ class BrowserServer {
#bar{padding:10px 5% 20px}
#waveform{width:160px;height:28px}
#text-container{font-size:clamp(28px,calc(100vw / 10),60px)}
#mirror-btn{padding:0 10px;min-width:36px}
#mirror-btn .label{display:none}
}
</style>
</head>
Expand All @@ -398,6 +410,10 @@ class BrowserServer {
<div id="waveform"></div>
<div id="spoken"></div>
<div id="mic-btn"><div id="mic-dot"></div></div>
<button id="mirror-btn" type="button" aria-pressed="false" title="Enable Mirror">
<span class="icon">⇋</span>
<span class="label">Mirror</span>
</button>
</div>
</div>

Expand All @@ -408,7 +424,8 @@ class BrowserServer {

<script>
const WSP=\(wsPort),host=location.hostname;
let ws,rt,prevWordKey='',scrollTgt=null;
const MIRROR_STORAGE_KEY='textream.remote.mirror';
let ws,rt,prevWordKey='',scrollTgt=null,mirrorEnabled=false;

/* ---- helpers ---- */

Expand All @@ -425,6 +442,43 @@ class BrowserServer {
}
function rgba(rgb,a){return 'rgba('+rgb[0]+','+rgb[1]+','+rgb[2]+','+a+')';}

function parseMirrorQueryParam(){
const raw=new URLSearchParams(location.search).get('mirror');
if(raw===null)return null;
const val=raw.toLowerCase();
if(val==='1'||val==='true'||val==='on'||val==='yes')return true;
if(val==='0'||val==='false'||val==='off'||val==='no')return false;
return null;
}

function applyMirrorMode(){
document.body.classList.toggle('mirrored',mirrorEnabled);
const btn=document.getElementById('mirror-btn');
if(!btn)return;
btn.classList.toggle('on',mirrorEnabled);
btn.setAttribute('aria-pressed',mirrorEnabled?'true':'false');
btn.title=mirrorEnabled?'Disable Mirror':'Enable Mirror';
}

function toggleMirrorMode(){
mirrorEnabled=!mirrorEnabled;
localStorage.setItem(MIRROR_STORAGE_KEY,mirrorEnabled?'1':'0');
applyMirrorMode();
}

function initializeMirrorMode(){
const override=parseMirrorQueryParam();
if(override!==null){
mirrorEnabled=override;
localStorage.setItem(MIRROR_STORAGE_KEY,mirrorEnabled?'1':'0');
}else{
mirrorEnabled=localStorage.getItem(MIRROR_STORAGE_KEY)==='1';
}
const btn=document.getElementById('mirror-btn');
if(btn)btn.addEventListener('click',toggleMirrorMode);
applyMirrorMode();
}

// Detect annotation words: [bracket] or emoji-only (no letters/digits)
function isAnnotation(w){
if(w.startsWith('[')&&w.endsWith(']'))return true;
Expand Down Expand Up @@ -592,6 +646,7 @@ class BrowserServer {
for(let i=0;i<30;i++){const b=document.createElement('div');
b.className='wf';b.style.height='2px';wfInit.appendChild(b)}

initializeMirrorMode();
connect();
</script>
</body>
Expand Down