mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-16 23:33:10 -06:00
v1
This commit is contained in:
parent
e1b444f252
commit
67c6e0a31a
|
|
@ -3,7 +3,7 @@
|
|||
android:color="#2563EB">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#3B82F6" />
|
||||
<solid android:color="#e65c00" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp" android:height="40dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#3B82F6" android:pathData="M3,4h18a1,1 0,0 1,1 1v11a1,1 0,0 1,-1 1H3a1,1 0,0 1,-1 -1V5a1,1 0,0 1,1 -1zM3,6v9h18V6z" />
|
||||
<path android:fillColor="#3B82F6" android:pathData="M8,20h8v2H8z" />
|
||||
<path android:fillColor="#e65c00" android:pathData="M3,4h18a1,1 0,0 1,1 1v11a1,1 0,0 1,-1 1H3a1,1 0,0 1,-1 -1V5a1,1 0,0 1,1 -1zM3,6v9h18V6z" />
|
||||
<path android:fillColor="#e65c00" android:pathData="M8,20h8v2H8z" />
|
||||
<path android:fillColor="#93C5FD" android:pathData="M12,10.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RemoteDisplay"
|
||||
android:textColor="#3B82F6"
|
||||
android:textColor="#e65c00"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@
|
|||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:thumbTint="#FFFFFF"
|
||||
android:trackTint="#3B82F6" />
|
||||
android:trackTint="#e65c00" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/openA11yBtn"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RemoteDisplay"
|
||||
android:textColor="#3B82F6"
|
||||
android:textColor="#e65c00"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="96dp"
|
||||
android:text="------"
|
||||
android:textColor="#3B82F6"
|
||||
android:textColor="#e65c00"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"
|
||||
android:letterSpacing="0.3"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RemoteDisplay Setup"
|
||||
android:textColor="#3B82F6"
|
||||
android:textColor="#e65c00"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="3dp" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#111827</color>
|
||||
<color name="ic_launcher_foreground">#3B82F6</color>
|
||||
<color name="ic_launcher_foreground">#e65c00</color>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.RemoteDisplay" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">#3B82F6</item>
|
||||
<item name="colorPrimary">#e65c00</item>
|
||||
<item name="colorPrimaryVariant">#2563EB</item>
|
||||
<item name="colorSecondary">#1E293B</item>
|
||||
<item name="android:windowBackground">#111827</item>
|
||||
|
|
|
|||
79
fix_fonts_emojis.js
Normal file
79
fix_fonts_emojis.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 1. Fix reset.css
|
||||
let resetCss = fs.readFileSync('frontend/css/reset.css', 'utf-8');
|
||||
resetCss = resetCss.replace(
|
||||
"font-family: 'Michroma', sans-serif;",
|
||||
"font-family: 'Michroma', sans-serif;\n font-weight: 400;"
|
||||
);
|
||||
resetCss = resetCss.replace(
|
||||
"h1, h2, h3, h4, h5, h6 { font-family: 'Goldman', sans-serif; }",
|
||||
"h1, h2, h3, h4, h5, h6 { font-family: 'Goldman', sans-serif; font-weight: 700; }"
|
||||
);
|
||||
fs.writeFileSync('frontend/css/reset.css', resetCss);
|
||||
|
||||
// 2. Fix landing.html fonts and emojis
|
||||
let landing = fs.readFileSync('frontend/landing.html', 'utf-8');
|
||||
if (!landing.includes('font-awesome')) {
|
||||
landing = landing.replace(
|
||||
'</head>',
|
||||
' <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">\n</head>'
|
||||
);
|
||||
}
|
||||
// Add fonts
|
||||
if (!landing.includes('Michroma')) {
|
||||
landing = landing.replace(
|
||||
'</head>',
|
||||
' <link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n <link href="https://fonts.googleapis.com/css2?family=Goldman:wght@700&family=Michroma:wght@400&display=swap" rel="stylesheet">\n</head>'
|
||||
);
|
||||
}
|
||||
|
||||
// Replace emojis with fontawesome icons manually based on the context
|
||||
const emojiMap = {
|
||||
'📺': '<i class="fa-solid fa-tv"></i>',
|
||||
'🎬': '<i class="fa-solid fa-border-all"></i>',
|
||||
'🖥': '<i class="fa-solid fa-desktop"></i>',
|
||||
'📅': '<i class="fa-regular fa-calendar"></i>',
|
||||
'🎶': '<i class="fa-solid fa-list"></i>',
|
||||
'🏢': '<i class="fa-solid fa-building"></i>',
|
||||
'🔧': '<i class="fa-solid fa-wrench"></i>',
|
||||
'🖱': '<i class="fa-solid fa-hand-pointer"></i>',
|
||||
'📊': '<i class="fa-solid fa-chart-line"></i>',
|
||||
'🔔': '<i class="fa-solid fa-bell"></i>',
|
||||
'🔌': '<i class="fa-solid fa-plug-circle-xmark"></i>',
|
||||
'📱': '<i class="fa-solid fa-mobile-screen"></i>',
|
||||
'🔒': '<i class="fa-solid fa-server"></i>',
|
||||
'🎨': '<i class="fa-solid fa-palette"></i>',
|
||||
'👥': '<i class="fa-solid fa-users"></i>',
|
||||
'🔄': '<i class="fa-solid fa-arrows-rotate"></i>',
|
||||
'🛒': '<i class="fa-solid fa-cart-shopping"></i>',
|
||||
'🍽': '<i class="fa-solid fa-utensils"></i>',
|
||||
'🏫': '<i class="fa-solid fa-graduation-cap"></i>',
|
||||
'🏥': '<i class="fa-solid fa-notes-medical"></i>',
|
||||
'⛪': '<i class="fa-solid fa-church"></i>',
|
||||
'🏋': '<i class="fa-solid fa-dumbbell"></i>',
|
||||
'🤖': '<i class="fa-brands fa-android"></i>',
|
||||
'🔥': '<i class="fa-brands fa-amazon"></i>',
|
||||
'🥏': '<i class="fa-brands fa-raspberry-pi"></i>',
|
||||
'💻': '<i class="fa-brands fa-windows"></i>',
|
||||
'🌐': '<i class="fa-brands fa-chrome"></i>'
|
||||
};
|
||||
|
||||
for (const [emoji, icon] of Object.entries(emojiMap)) {
|
||||
landing = landing.split(emoji).join(icon);
|
||||
}
|
||||
|
||||
// Add CSS to landing page for fonts
|
||||
landing = landing.replace(
|
||||
'</style>',
|
||||
` body { font-family: 'Michroma', sans-serif; font-weight: 400; }
|
||||
h1, h2, h3, h4, h5, h6, .brand { font-family: 'Goldman', sans-serif; font-weight: 700; }
|
||||
.feature-icon { font-size: 32px; color: var(--accent); margin-bottom: 16px; }
|
||||
.platform-item .icon { font-size: 32px; color: var(--accent); margin-bottom: 8px; }
|
||||
</style>`
|
||||
);
|
||||
|
||||
fs.writeFileSync('frontend/landing.html', landing);
|
||||
|
||||
console.log('Fixed fonts and emojis');
|
||||
20
fix_landing.js
Normal file
20
fix_landing.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const fs = require('fs');
|
||||
|
||||
let landing = fs.readFileSync('frontend/landing.html', 'utf-8');
|
||||
|
||||
// Replace '$' with '€' in the specific lines
|
||||
landing = landing.replace('$1,188', '€1,188');
|
||||
landing = landing.replace('$1,440+', '€1,440+');
|
||||
landing = landing.replace('$3,600+', '€3,600+');
|
||||
landing = landing.replace('$1,800+', '€1,800+');
|
||||
landing = landing.replace("? '$' + p.price_monthly", "? '€' + p.price_monthly");
|
||||
landing = landing.replace('or $$', 'or €$'); // Actually it's or $${p.price_yearly}, so it becomes or €${p.price_yearly}
|
||||
|
||||
// Remove "ScreenTinker" from the header
|
||||
landing = landing.replace(
|
||||
'<span class="nav-logo-text" style="margin-left: 8px;">ScreenTinker</span>',
|
||||
''
|
||||
);
|
||||
|
||||
fs.writeFileSync('frontend/landing.html', landing);
|
||||
console.log('Fixed landing.html');
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Michroma', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
|
|
@ -14,7 +15,7 @@ html, body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 { font-family: 'Goldman', sans-serif; }
|
||||
h1, h2, h3, h4, h5, h6 { font-family: 'Goldman', sans-serif; font-weight: 700; }
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
ul { list-style: none; }
|
||||
|
|
|
|||
|
|
@ -1,87 +1,409 @@
|
|||
/* Shared styles for SEO landing pages: comparison and guide pages.
|
||||
Matches the dark theme of landing.html. */
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root { --accent:#3b82f6; --bg:#111827; --card:#1e293b; --border:#334155; --text:#f1f5f9; --muted:#94a3b8; --dim:#64748b; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.65; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent: #e65c00;
|
||||
--bg: #111827;
|
||||
--card: #1e293b;
|
||||
--border: #334155;
|
||||
--text: #f1f5f9;
|
||||
--muted: #94a3b8;
|
||||
--dim: #64748b;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Nav (matches landing.html) */
|
||||
nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(17,24,39,0.9); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
|
||||
.nav-inner { max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.nav-logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 18px; color: var(--accent); flex-shrink: 0; }
|
||||
.nav-logo a { color: var(--accent); }
|
||||
.nav-links { display: flex; align-items: center; flex-wrap: nowrap; }
|
||||
.nav-links a { color: var(--muted); margin-left: 24px; font-size: 14px; transition: color 0.2s; }
|
||||
.nav-links a:hover { color: var(--text); text-decoration: none; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.2s; border: none; cursor: pointer; }
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: #2563eb; text-decoration: none; }
|
||||
.btn-outline { background: transparent; color: var(--accent); border: 1px solid var(--accent); }
|
||||
.btn-outline:hover { background: rgba(59,130,246,0.1); text-decoration: none; }
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-logo a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--muted);
|
||||
margin-left: 24px;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Article container */
|
||||
.article { max-width: 880px; margin: 0 auto; padding: 120px 24px 60px; }
|
||||
.breadcrumb { font-size: 13px; color: var(--muted); margin-bottom: 24px; }
|
||||
.breadcrumb a { color: var(--muted); }
|
||||
.breadcrumb a:hover { color: var(--text); }
|
||||
.breadcrumb span { margin: 0 8px; color: var(--dim); }
|
||||
.article {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 120px 24px 60px;
|
||||
}
|
||||
|
||||
.article h1 { font-size: clamp(30px, 4vw, 44px); font-weight: 800; line-height: 1.2; margin-bottom: 16px; }
|
||||
.article .lead { font-size: 18px; color: var(--muted); margin-bottom: 32px; }
|
||||
.article h2 { font-size: 28px; font-weight: 700; margin: 48px 0 16px; line-height: 1.3; }
|
||||
.article h3 { font-size: 20px; font-weight: 600; margin: 28px 0 12px; }
|
||||
.article p { margin-bottom: 16px; color: var(--text); }
|
||||
.article ul, .article ol { margin: 0 0 16px 24px; color: var(--text); }
|
||||
.article li { margin-bottom: 8px; }
|
||||
.article strong { color: var(--text); font-weight: 600; }
|
||||
.article code { background: var(--card); border: 1px solid var(--border); padding: 2px 6px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; color: #e2e8f0; }
|
||||
.article pre { background: var(--card); border: 1px solid var(--border); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; }
|
||||
.article pre code { background: transparent; border: none; padding: 0; font-size: 13px; }
|
||||
.article blockquote { border-left: 3px solid var(--accent); padding: 8px 16px; margin: 16px 0; color: var(--muted); background: rgba(59,130,246,0.06); border-radius: 4px; }
|
||||
.breadcrumb {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
margin: 0 8px;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.article h1 {
|
||||
font-size: clamp(30px, 4vw, 44px);
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.article .lead {
|
||||
font-size: 18px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.article h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 48px 0 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.article h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 28px 0 12px;
|
||||
}
|
||||
|
||||
.article p {
|
||||
margin-bottom: 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.article ul,
|
||||
.article ol {
|
||||
margin: 0 0 16px 24px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.article li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.article strong {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.article code {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.article pre {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.article pre code {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.article blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding: 8px 16px;
|
||||
margin: 16px 0;
|
||||
color: var(--muted);
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Comparison table */
|
||||
.compare-table-wrap { width: 100%; overflow-x: auto; margin: 24px 0; -webkit-overflow-scrolling: touch; }
|
||||
.compare-table { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 640px; }
|
||||
.compare-table th, .compare-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
.compare-table th { color: var(--text); font-weight: 600; background: var(--card); }
|
||||
.compare-table td:first-child { color: var(--muted); }
|
||||
.compare-table .yes { color: #22c55e; font-weight: 600; }
|
||||
.compare-table .no { color: #ef4444; }
|
||||
.compare-table .partial { color: #f59e0b; }
|
||||
.compare-table tbody tr:hover { background: rgba(59,130,246,0.04); }
|
||||
.compare-table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin: 24px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.compare-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.compare-table th,
|
||||
.compare-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.compare-table th {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.compare-table td:first-child {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.compare-table .yes {
|
||||
color: #22c55e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.compare-table .no {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.compare-table .partial {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.compare-table tbody tr:hover {
|
||||
background: rgba(59, 130, 246, 0.04);
|
||||
}
|
||||
|
||||
/* CTA */
|
||||
.cta { text-align: center; padding: 60px 24px; background: linear-gradient(135deg, rgba(59,130,246,0.1), rgba(139,92,246,0.1)); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); margin: 48px 0; border-radius: 12px; }
|
||||
.cta h2 { font-size: 28px; margin: 0 0 12px; }
|
||||
.cta p { color: var(--muted); margin-bottom: 20px; font-size: 17px; }
|
||||
.cta {
|
||||
text-align: center;
|
||||
padding: 60px 24px;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin: 48px 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.cta h2 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.cta p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 20px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
/* Related links / internal linking block */
|
||||
.related { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin: 32px 0; }
|
||||
.related h2 { margin: 0 0 12px; font-size: 20px; }
|
||||
.related ul { margin: 0; list-style: none; }
|
||||
.related li { margin: 8px 0; padding-left: 0; }
|
||||
.related li::before { content: '> '; color: var(--accent); font-weight: 700; }
|
||||
.related {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.related h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.related ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.related li {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.related li::before {
|
||||
content: '> ';
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Footer (matches landing.html) */
|
||||
footer { max-width: 1200px; margin: 0 auto; padding: 40px 24px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; border-top: 1px solid var(--border); }
|
||||
footer .links a { color: var(--dim); margin-left: 16px; font-size: 13px; }
|
||||
footer .links a:hover { color: var(--text); text-decoration: none; }
|
||||
footer {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
footer .links a {
|
||||
color: var(--dim);
|
||||
margin-left: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
footer .links a:hover {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.nav-links a:not(.btn) { display: none; }
|
||||
.nav-inner { padding: 12px 14px; gap: 8px; }
|
||||
.nav-links .btn { padding: 8px 12px; font-size: 13px; margin-left: 8px; flex-shrink: 0; min-height: 0; }
|
||||
.btn { min-height: 44px; }
|
||||
.article { padding: 100px 16px 40px; }
|
||||
.cta { padding: 40px 16px; }
|
||||
footer { flex-direction: column; text-align: center; }
|
||||
footer .links a { margin: 4px 8px; }
|
||||
.compare-table { font-size: 12px; }
|
||||
.compare-table th, .compare-table td { padding: 8px; }
|
||||
.nav-links a:not(.btn) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
padding: 12px 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-links .btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.article {
|
||||
padding: 100px 16px 40px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .links a {
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
.compare-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.compare-table th,
|
||||
.compare-table td {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.nav-logo-text { display: none; }
|
||||
.nav-logo-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html data-theme="dark" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
|
@ -10,11 +11,12 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" href="/assets/icon-192.png">
|
||||
<link rel="apple-touch-icon" href="/assets/icon-192.png">
|
||||
<title>GoldenSignage</title>
|
||||
<title>SwiftDisplay</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Goldman:wght@400;700&family=Michroma&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Goldman:wght@400;700&family=Michroma&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/reset.css">
|
||||
|
|
@ -22,16 +24,22 @@
|
|||
<script src="/socket.io/socket.io.js"></script>
|
||||
<!-- OAuth providers loaded on-demand by login.js when needed -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button class="mobile-menu-btn" id="mobileMenuBtn" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
<button class="mobile-menu-btn" id="mobileMenuBtn" aria-label="Toggle navigation menu" aria-expanded="false"
|
||||
aria-controls="sidebar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<img src="/assets/logowhiteswift.png" alt="Logo" style="height: 28px;">
|
||||
<span id="brandName" style="margin-left: 8px;">GoldenSignage</span>
|
||||
<span id="brandName" style="margin-left: 8px;">SwiftDisplay</span>
|
||||
</div>
|
||||
<!-- #38: apply cached white-label before first paint (no ScreenTinker flash) -->
|
||||
<script src="/js/brand-prime.js"></script>
|
||||
|
|
@ -40,8 +48,10 @@
|
|||
<ul class="nav-links">
|
||||
<li><a href="#/" class="nav-link active" data-view="dashboard">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
<span>Displays</span>
|
||||
</a></li>
|
||||
|
|
@ -54,53 +64,72 @@
|
|||
</a></li>
|
||||
<li><a href="#/playlists" class="nav-link" data-view="playlists">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</svg>
|
||||
<span>Playlists</span>
|
||||
</a></li>
|
||||
<li><a href="#/layouts" class="nav-link" data-view="layouts">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</svg>
|
||||
<span>Layouts</span>
|
||||
</a></li>
|
||||
<li><a href="#/widgets" class="nav-link" data-view="widgets">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/>
|
||||
<rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>
|
||||
<rect x="2" y="2" width="8" height="8" rx="1" />
|
||||
<rect x="14" y="2" width="8" height="8" rx="1" />
|
||||
<rect x="2" y="14" width="8" height="8" rx="1" />
|
||||
<rect x="14" y="14" width="8" height="8" rx="1" />
|
||||
</svg>
|
||||
<span>Widgets</span>
|
||||
</a></li>
|
||||
<li><a href="#/schedule" class="nav-link" data-view="schedule">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
<span>Schedule</span>
|
||||
</a></li>
|
||||
<li><a href="#/walls" class="nav-link" data-view="walls">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="9" height="8" rx="1"/><rect x="13" y="3" width="9" height="8" rx="1"/>
|
||||
<rect x="2" y="13" width="9" height="8" rx="1"/><rect x="13" y="13" width="9" height="8" rx="1"/>
|
||||
<rect x="2" y="3" width="9" height="8" rx="1" />
|
||||
<rect x="13" y="3" width="9" height="8" rx="1" />
|
||||
<rect x="2" y="13" width="9" height="8" rx="1" />
|
||||
<rect x="13" y="13" width="9" height="8" rx="1" />
|
||||
</svg>
|
||||
<span>Video Walls</span>
|
||||
</a></li>
|
||||
<li><a href="#/reports" class="nav-link" data-view="reports">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>
|
||||
<line x1="18" y1="20" x2="18" y2="10" />
|
||||
<line x1="12" y1="20" x2="12" y2="4" />
|
||||
<line x1="6" y1="20" x2="6" y2="14" />
|
||||
</svg>
|
||||
<span>Reports</span>
|
||||
</a></li>
|
||||
<li><a href="#/kiosk" class="nav-link" data-view="kiosk">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8"/><path d="M12 17v4"/>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||
<path d="M8 21h8" />
|
||||
<path d="M12 17v4" />
|
||||
<circle cx="12" cy="10" r="1" />
|
||||
</svg>
|
||||
<span>Kiosk</span>
|
||||
</a></li>
|
||||
<li><a href="#/designer" class="nav-link" data-view="designer">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/>
|
||||
<path d="M12 19l7-7 3 3-7 7-3-3z" />
|
||||
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" />
|
||||
<path d="M2 2l7.586 7.586" />
|
||||
<circle cx="11" cy="11" r="2" />
|
||||
</svg>
|
||||
<span>Designer</span>
|
||||
|
|
@ -116,21 +145,26 @@
|
|||
bookmark still loads (and shows the 503 from the API). -->
|
||||
<li style="display:none"><a href="#/teams" class="nav-link" data-view="teams">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
<span>Teams</span>
|
||||
</a></li>
|
||||
<li><a href="#/help" class="nav-link" data-view="help">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<span>Help</span>
|
||||
</a></li>
|
||||
<li><a href="#/settings" class="nav-link" data-view="settings">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</a></li>
|
||||
|
|
@ -171,41 +205,55 @@
|
|||
<h3 data-i18n="add_display.title">Add Display</h3>
|
||||
<button class="btn-icon" data-close-modal="addDeviceModal">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="modal-description" style="margin-bottom:16px" data-i18n="add_display.intro">Enter the 6-digit pairing code shown on the display.</p>
|
||||
<p class="modal-description" style="margin-bottom:16px" data-i18n="add_display.intro">Enter the 6-digit pairing
|
||||
code shown on the display.</p>
|
||||
<div class="form-group">
|
||||
<label data-i18n="add_display.pairing_code">Pairing Code</label>
|
||||
<input type="text" id="pairingCodeInput" maxlength="6" pattern="[0-9]{6}" placeholder="000000" class="pairing-input">
|
||||
<input type="text" id="pairingCodeInput" maxlength="6" pattern="[0-9]{6}" placeholder="000000"
|
||||
class="pairing-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="add_display.display_name">Display Name (optional)</label>
|
||||
<input type="text" id="deviceNameInput" data-i18n-placeholder="add_display.name_placeholder" placeholder="e.g., Lobby TV" class="input">
|
||||
<input type="text" id="deviceNameInput" data-i18n-placeholder="add_display.name_placeholder"
|
||||
placeholder="e.g., Lobby TV" class="input">
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border,#1e293b);margin-top:20px;padding-top:16px">
|
||||
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500" data-i18n="add_display.need_player">Need a player app? Install one to get a pairing code:</p>
|
||||
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500"
|
||||
data-i18n="add_display.need_player">Need a player app? Install one to get a pairing code:</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<a href="/download/apk" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
<a href="/download/apk" class="btn btn-secondary btn-sm"
|
||||
style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
🤖 <span data-i18n="add_display.android_apk">Android APK</span>
|
||||
</a>
|
||||
<a href="/player" target="_blank" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
<a href="/player" target="_blank" class="btn btn-secondary btn-sm"
|
||||
style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
🌐 <span data-i18n="add_display.web_player">Web Player</span>
|
||||
</a>
|
||||
<a href="/scripts/raspberry-pi-setup.sh" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
<a href="/scripts/raspberry-pi-setup.sh" class="btn btn-secondary btn-sm"
|
||||
style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
🥏 <span data-i18n="add_display.raspberry_pi">Raspberry Pi</span>
|
||||
</a>
|
||||
<a href="/scripts/windows-setup.bat" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
<a href="/scripts/windows-setup.bat" class="btn btn-secondary btn-sm"
|
||||
style="text-decoration:none;justify-content:center;font-size:12px">
|
||||
💻 <span data-i18n="add_display.windows">Windows</span>
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-muted,#64748b);margin-top:8px" data-i18n-html="add_display.smart_tv_note">Smart TVs (LG/Samsung): open the built-in browser and navigate to <code style="background:var(--bg-input,#0f172a);padding:1px 4px;border-radius:3px">/player</code></p>
|
||||
<p style="font-size:11px;color:var(--text-muted,#64748b);margin-top:8px"
|
||||
data-i18n-html="add_display.smart_tv_note">Smart TVs (LG/Samsung): open the built-in browser and navigate to
|
||||
<code style="background:var(--bg-input,#0f172a);padding:1px 4px;border-radius:3px">/player</code></p>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border,#1e293b);margin-top:16px;padding-top:16px">
|
||||
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500" data-i18n="add_display.owner_prompt">Provisioning a fresh / factory-reset Android panel? Enroll it as device owner for silent updates, reboot & kiosk:</p>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="deviceOwnerQrBtn" style="width:100%;justify-content:center;font-size:12px">
|
||||
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500"
|
||||
data-i18n="add_display.owner_prompt">Provisioning a fresh / factory-reset Android panel? Enroll it as device
|
||||
owner for silent updates, reboot & kiosk:</p>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="deviceOwnerQrBtn"
|
||||
style="width:100%;justify-content:center;font-size:12px">
|
||||
📲 <span data-i18n="add_display.owner_qr_btn">Show device-owner QR</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -222,4 +270,5 @@
|
|||
|
||||
<script type="module" src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -183,7 +183,7 @@ async function loadSummary() {
|
|||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
const families = [
|
||||
['Tizen', data.byFamily.tizen, '#3b82f6'],
|
||||
['Tizen', data.byFamily.tizen, '#e65c00'],
|
||||
['WebOS', data.byFamily.webos, '#a3e635'],
|
||||
['Fire TV', data.byFamily.fire_tv, '#f97316'],
|
||||
['Bravia', data.byFamily.bravia, '#a855f7'],
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ async function loadBranding() {
|
|||
el.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;max-width:640px">
|
||||
<div class="form-group" style="grid-column:1/-1"><label>${t('admin.branding.brand_name')}</label><input type="text" id="brBrandName" class="input" placeholder="ScreenTinker" value="${v(b.brand_name)}"></div>
|
||||
<div class="form-group"><label>${t('admin.branding.primary_color')}</label><input type="text" id="brPrimary" class="input" placeholder="#3B82F6" value="${v(b.primary_color)}"></div>
|
||||
<div class="form-group"><label>${t('admin.branding.primary_color')}</label><input type="text" id="brPrimary" class="input" placeholder="#e65c00" value="${v(b.primary_color)}"></div>
|
||||
<div class="form-group"><label>${t('admin.branding.bg_color')}</label><input type="text" id="brBg" class="input" placeholder="#111827" value="${v(b.bg_color)}"></div>
|
||||
<div class="form-group" style="grid-column:1/-1"><label>${t('admin.branding.logo_url')}</label><input type="text" id="brLogo" class="input" placeholder="https://…/logo.png" value="${v(b.logo_url)}"></div>
|
||||
<div class="form-group" style="grid-column:1/-1"><label>${t('admin.branding.favicon_url')}</label><input type="text" id="brFavicon" class="input" placeholder="https://…/favicon.ico" value="${v(b.favicon_url)}"></div>
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ function renderGroupSection(group, devices, playlists) {
|
|||
const playlistLabel = getGroupPlaylistLabel(devices, playlists);
|
||||
return `
|
||||
<div class="group-section" data-group-id="${group.id}" style="margin-bottom:24px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding:8px 12px;background:var(--bg-secondary);border-radius:8px;border-left:4px solid ${esc(group.color || '#3B82F6')}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding:8px 12px;background:var(--bg-secondary);border-radius:8px;border-left:4px solid ${esc(group.color || '#e65c00')}">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<strong style="font-size:15px">${esc(group.name)}</strong>
|
||||
<span style="color:var(--text-muted);font-size:12px">${tn('dashboard.devices_count', devices.length)} · ${t('dashboard.online_count', { n: onlineCount })}</span>
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export function render(container, widgetId) {
|
|||
const url = prompt(t('designer.prompt.rss_url'), 'https://feeds.bbci.co.uk/news/rss.xml');
|
||||
if (url) addElement({ type: 'ticker', x: 0, y: 90, width: 100, height: 10, feedUrl: url, speed: 30, fontSize: 20, color: '#FFFFFF', bgColor: 'rgba(0,0,0,0.7)' });
|
||||
};
|
||||
document.getElementById('addShape').onclick = () => addElement({ type: 'shape', x: 20, y: 20, width: 30, height: 20, color: '#3b82f6', opacity: 0.7, radius: 8, shape: 'rect' });
|
||||
document.getElementById('addShape').onclick = () => addElement({ type: 'shape', x: 20, y: 20, width: 30, height: 20, color: '#e65c00', opacity: 0.7, radius: 8, shape: 'rect' });
|
||||
document.getElementById('addQR').onclick = () => {
|
||||
const data = prompt(t('designer.prompt.qr_url'), 'https://example.com');
|
||||
if (data) addElement({ type: 'qr', x: 80, y: 70, size: 15, data, fgColor: '#FFFFFF', bgColor: '#000000' });
|
||||
|
|
@ -538,7 +538,7 @@ function redraw() {
|
|||
// Elements
|
||||
elements.forEach((el, i) => {
|
||||
const selected = i === selectedIdx;
|
||||
const border = selected ? 'outline:2px solid #3b82f6;outline-offset:2px;' : '';
|
||||
const border = selected ? 'outline:2px solid #e65c00;outline-offset:2px;' : '';
|
||||
const cursor = 'cursor:move;';
|
||||
|
||||
switch (el.type) {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ async function renderEditor(container, pageId) {
|
|||
<option ${(config.style?.columns || 3) === 4 ? 'selected' : ''} value="4">4</option>
|
||||
</select></div>
|
||||
<div class="form-group"><label>${t('kiosk.button_color')}</label><input type="color" id="kBtnBg" value="${config.style?.buttonBg || '#1e293b'}" style="width:100%;height:28px;border:none;cursor:pointer"></div>
|
||||
<div class="form-group"><label>${t('kiosk.button_hover')}</label><input type="color" id="kBtnHover" value="${config.style?.buttonHover || '#3b82f6'}" style="width:100%;height:28px;border:none;cursor:pointer"></div>
|
||||
<div class="form-group"><label>${t('kiosk.button_hover')}</label><input type="color" id="kBtnHover" value="${config.style?.buttonHover || '#e65c00'}" style="width:100%;height:28px;border:none;cursor:pointer"></div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export async function render(container) {
|
|||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>${t('schedule.priority')}</label><input type="number" id="schedPriority" class="input" value="0" min="0" max="100"></div>
|
||||
<div class="form-group"><label>${t('schedule.color')}</label><input type="color" id="schedColor" value="#3B82F6" style="width:60px;height:32px;border:none;cursor:pointer"></div>
|
||||
<div class="form-group"><label>${t('schedule.color')}</label><input type="color" id="schedColor" value="#e65c00" style="width:60px;height:32px;border:none;cursor:pointer"></div>
|
||||
</div>
|
||||
<div class="modal-footer" style="display:flex;justify-content:space-between;gap:8px">
|
||||
<button class="btn btn-danger" id="deleteScheduleBtn" style="display:none">${t('common.delete')}</button>
|
||||
|
|
@ -190,7 +190,7 @@ export async function render(container) {
|
|||
const block = document.createElement('div');
|
||||
const topOffset = (startHour - Math.floor(startHour)) * 28;
|
||||
block.style.cssText = `position:absolute;top:${topOffset}px;left:2px;right:2px;height:${Math.max(20, duration * 28)}px;
|
||||
background:${ev.color || '#3B82F6'};border-radius:3px;padding:2px 4px;font-size:10px;color:white;overflow:hidden;cursor:pointer;z-index:1;opacity:0.85;
|
||||
background:${ev.color || '#e65c00'};border-radius:3px;padding:2px 4px;font-size:10px;color:white;overflow:hidden;cursor:pointer;z-index:1;opacity:0.85;
|
||||
${isGroupSchedule ? 'border:1.5px dashed rgba(255,255,255,0.6);' : ''}`;
|
||||
|
||||
const label = ev.title || ev.playlist_name || ev.content_name || ev.widget_name || t('schedule.scheduled_label');
|
||||
|
|
@ -215,7 +215,7 @@ export async function render(container) {
|
|||
document.getElementById('schedEnd').value = `${String(end.getHours()).padStart(2, '0')}:${String(end.getMinutes()).padStart(2, '0')}`;
|
||||
document.getElementById('schedRepeat').value = ev.recurrence || '';
|
||||
document.getElementById('schedPriority').value = ev.priority || 0;
|
||||
document.getElementById('schedColor').value = ev.color || '#3B82F6';
|
||||
document.getElementById('schedColor').value = ev.color || '#e65c00';
|
||||
|
||||
if (ev.group_id) {
|
||||
groupRadio.checked = true;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export async function render(container) {
|
|||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group"><label>${t('settings.brand_name')}</label><input type="text" id="wlBrandName" class="input" placeholder="ScreenTinker"></div>
|
||||
<div class="form-group"><label>${t('settings.logo_url')}</label><input type="text" id="wlLogoUrl" class="input" placeholder="https://..."></div>
|
||||
<div class="form-group"><label>${t('settings.primary_color')}</label><input type="color" id="wlPrimaryColor" value="#3B82F6" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
||||
<div class="form-group"><label>${t('settings.primary_color')}</label><input type="color" id="wlPrimaryColor" value="#e65c00" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
||||
<div class="form-group"><label>${t('settings.bg_color')}</label><input type="color" id="wlBgColor" value="#111827" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
||||
<div class="form-group"><label>${t('settings.custom_domain')}</label><input type="text" id="wlDomain" class="input" placeholder="signage.yourcompany.com"></div>
|
||||
<div class="form-group"><label>${t('settings.favicon_url')}</label><input type="text" id="wlFavicon" class="input" placeholder="https://..."></div>
|
||||
|
|
@ -373,8 +373,7 @@ export async function render(container) {
|
|||
<tr style="border-bottom:1px solid var(--border)${tok.revoked_at ? ';opacity:0.55' : ''}">
|
||||
<td style="padding:10px 12px;font-family:monospace">${esc(tok.prefix)}…</td>
|
||||
<td style="padding:10px 12px">${esc(tok.name || '')}</td>
|
||||
<td style="padding:10px 12px">${esc(scopeLabel(tok.scope))}${
|
||||
tok.scope === 'agency' && Array.isArray(tok.targets)
|
||||
<td style="padding:10px 12px">${esc(scopeLabel(tok.scope))}${tok.scope === 'agency' && Array.isArray(tok.targets)
|
||||
? `<div style="font-size:11px;color:var(--text-muted);margin-top:2px">${t('apitoken.targets_label')} ${tok.targets.length ? tok.targets.map(p => esc(p.name)).join(', ') : '—'}${tok.auto_publish ? ' · ' + esc(t('apitoken.auto_publish_on')) : ''}</div><div style="font-size:11px;color:var(--text-muted);margin-top:2px">${t('apitoken.folder_label')} ${tok.upload_folder ? esc(tok.upload_folder) : esc(t('apitoken.folder_root'))}</div>`
|
||||
: ''}</td>
|
||||
<td style="padding:10px 12px">${esc(fmtTokenDate(tok.created_at))}</td>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy - ScreenTinker</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,sans-serif; background:#111827; color:#e2e8f0; line-height:1.8; }
|
||||
.container { max-width:800px; margin:0 auto; padding:40px 24px 80px; }
|
||||
h1 { color:#3b82f6; font-size:32px; margin-bottom:8px; }
|
||||
.updated { color:#64748b; font-size:14px; margin-bottom:40px; }
|
||||
h2 { color:#f1f5f9; font-size:20px; margin:32px 0 12px; }
|
||||
h3 { color:#cbd5e1; font-size:16px; margin:20px 0 8px; }
|
||||
p, li { color:#94a3b8; font-size:15px; margin-bottom:12px; }
|
||||
ul { padding-left:24px; }
|
||||
a { color:#3b82f6; }
|
||||
.back { display:inline-flex; align-items:center; gap:6px; color:#64748b; font-size:13px; margin-bottom:24px; text-decoration:none; }
|
||||
.back:hover { color:#94a3b8; }
|
||||
table { width:100%; border-collapse:collapse; margin:16px 0; }
|
||||
th, td { padding:10px 12px; text-align:left; border-bottom:1px solid #1e293b; font-size:14px; color:#94a3b8; }
|
||||
th { color:#cbd5e1; font-weight:600; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 80px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #e65c00;
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.updated {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #f1f5f9;
|
||||
font-size: 20px;
|
||||
margin: 32px 0 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #cbd5e1;
|
||||
font-size: 16px;
|
||||
margin: 20px 0 8px;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
color: #94a3b8;
|
||||
font-size: 15px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #e65c00;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin-bottom: 24px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back">← Back to ScreenTinker</a>
|
||||
|
|
@ -29,65 +107,157 @@
|
|||
<p class="updated">Last updated: May 15, 2026</p>
|
||||
|
||||
<h2>1. Overview</h2>
|
||||
<p>ScreenTinker ("we", "us", "our") respects your privacy. This policy explains what data we collect, how we use it, and your rights regarding your information.</p>
|
||||
<p>ScreenTinker ("we", "us", "our") respects your privacy. This policy explains what data we collect, how we use it,
|
||||
and your rights regarding your information.</p>
|
||||
|
||||
<h2>2. Information We Collect</h2>
|
||||
|
||||
<h3>2.1 Account Information</h3>
|
||||
<table>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
<tr><td>Email address</td><td>Authentication, notifications</td><td>Until account deletion</td></tr>
|
||||
<tr><td>Name</td><td>Display in dashboard</td><td>Until account deletion</td></tr>
|
||||
<tr><td>Password hash</td><td>Authentication (bcrypt, never stored in plain text)</td><td>Until account deletion</td></tr>
|
||||
<tr><td>OAuth provider ID</td><td>Google/Microsoft sign-in</td><td>Until account deletion</td></tr>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Purpose</th>
|
||||
<th>Retention</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Email address</td>
|
||||
<td>Authentication, notifications</td>
|
||||
<td>Until account deletion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Display in dashboard</td>
|
||||
<td>Until account deletion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password hash</td>
|
||||
<td>Authentication (bcrypt, never stored in plain text)</td>
|
||||
<td>Until account deletion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OAuth provider ID</td>
|
||||
<td>Google/Microsoft sign-in</td>
|
||||
<td>Until account deletion</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>2.2 Device Information</h3>
|
||||
<table>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
<tr><td>Device ID</td><td>Unique device identification</td><td>Until device removal</td></tr>
|
||||
<tr><td>IP address</td><td>Network connectivity, security</td><td>Overwritten each connection</td></tr>
|
||||
<tr><td>Android version, screen resolution</td><td>Compatibility, display optimization</td><td>Until device removal</td></tr>
|
||||
<tr><td>Battery, storage, RAM, CPU, WiFi</td><td>Device health monitoring</td><td>90 days (rolling)</td></tr>
|
||||
<tr><td>Device fingerprint (hardware hash)</td><td>Prevent trial abuse</td><td>Until device removal</td></tr>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Purpose</th>
|
||||
<th>Retention</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device ID</td>
|
||||
<td>Unique device identification</td>
|
||||
<td>Until device removal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP address</td>
|
||||
<td>Network connectivity, security</td>
|
||||
<td>Overwritten each connection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Android version, screen resolution</td>
|
||||
<td>Compatibility, display optimization</td>
|
||||
<td>Until device removal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Battery, storage, RAM, CPU, WiFi</td>
|
||||
<td>Device health monitoring</td>
|
||||
<td>90 days (rolling)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device fingerprint (hardware hash)</td>
|
||||
<td>Prevent trial abuse</td>
|
||||
<td>Until device removal</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>2.3 Usage Data</h3>
|
||||
<table>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
<tr><td>Content play logs</td><td>Proof-of-play reporting</td><td>90 days</td></tr>
|
||||
<tr><td>Activity log (API actions)</td><td>Audit trail, security</td><td>90 days</td></tr>
|
||||
<tr><td>Screenshots (on-demand)</td><td>Remote monitoring</td><td>Latest only per device</td></tr>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Purpose</th>
|
||||
<th>Retention</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Content play logs</td>
|
||||
<td>Proof-of-play reporting</td>
|
||||
<td>90 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Activity log (API actions)</td>
|
||||
<td>Audit trail, security</td>
|
||||
<td>90 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Screenshots (on-demand)</td>
|
||||
<td>Remote monitoring</td>
|
||||
<td>Latest only per device</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>2.4 Content</h3>
|
||||
<p>Media files (images, videos) you upload are stored on our servers solely to deliver them to your devices. We do not analyze, sell, or share your content.</p>
|
||||
<p>Media files (images, videos) you upload are stored on our servers solely to deliver them to your devices. We do
|
||||
not analyze, sell, or share your content.</p>
|
||||
|
||||
<h3>2.5 Error and Diagnostic Telemetry</h3>
|
||||
<p>Player clients submit error reports automatically so we can fix issues on devices we cannot directly access (smart TVs, embedded signage browsers). Reports may be submitted before a device has paired; in that case only the network IP and user-agent are present, with no device ID.</p>
|
||||
<p>Player clients submit error reports automatically so we can fix issues on devices we cannot directly access
|
||||
(smart TVs, embedded signage browsers). Reports may be submitted before a device has paired; in that case only the
|
||||
network IP and user-agent are present, with no device ID.</p>
|
||||
<table>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
<tr><td>Browser user-agent string</td><td>Identifying which player platforms encounter errors</td><td>Rolling buffer (10,000 entries)</td></tr>
|
||||
<tr><td>Error messages, stack traces, and source-file references</td><td>Diagnosing player issues we cannot reproduce in development</td><td>Rolling buffer (10,000 entries)</td></tr>
|
||||
<tr><td>Recent player log entries (up to 50, including filenames of content being played)</td><td>Context for the error so we understand what the player was doing</td><td>Rolling buffer (10,000 entries)</td></tr>
|
||||
<tr><td>Screen and viewport dimensions</td><td>Diagnosing layout and rendering issues at the device's actual size</td><td>Rolling buffer (10,000 entries)</td></tr>
|
||||
<tr><td>Coarse player state (idle, playing, waiting)</td><td>Distinguishing errors during playback vs setup</td><td>Rolling buffer (10,000 entries)</td></tr>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Purpose</th>
|
||||
<th>Retention</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Browser user-agent string</td>
|
||||
<td>Identifying which player platforms encounter errors</td>
|
||||
<td>Rolling buffer (10,000 entries)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Error messages, stack traces, and source-file references</td>
|
||||
<td>Diagnosing player issues we cannot reproduce in development</td>
|
||||
<td>Rolling buffer (10,000 entries)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Recent player log entries (up to 50, including filenames of content being played)</td>
|
||||
<td>Context for the error so we understand what the player was doing</td>
|
||||
<td>Rolling buffer (10,000 entries)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Screen and viewport dimensions</td>
|
||||
<td>Diagnosing layout and rendering issues at the device's actual size</td>
|
||||
<td>Rolling buffer (10,000 entries)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Coarse player state (idle, playing, waiting)</td>
|
||||
<td>Distinguishing errors during playback vs setup</td>
|
||||
<td>Rolling buffer (10,000 entries)</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>This telemetry can be disabled by self-hosters via the <code>PLAYER_DEBUG_REPORTING=off</code> environment variable. On screentinker.com it is on by default.</p>
|
||||
<p>This telemetry can be disabled by self-hosters via the <code>PLAYER_DEBUG_REPORTING=off</code> environment
|
||||
variable. On screentinker.com it is on by default.</p>
|
||||
|
||||
<h2>3. How We Use Your Information</h2>
|
||||
<ul>
|
||||
<li><strong>Provide the Service:</strong> Deliver content to devices, enable remote management, process subscriptions</li>
|
||||
<li><strong>Provide the Service:</strong> Deliver content to devices, enable remote management, process
|
||||
subscriptions</li>
|
||||
<li><strong>Security:</strong> Detect unauthorized access, prevent abuse, protect accounts</li>
|
||||
<li><strong>Communications:</strong> Send device offline alerts, subscription notifications, service updates</li>
|
||||
<li><strong>Improvement:</strong> Analyze aggregate usage patterns to improve the Service (no individual tracking)</li>
|
||||
<li><strong>Improvement:</strong> Analyze aggregate usage patterns to improve the Service (no individual tracking)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Data Sharing</h2>
|
||||
<p>We do not sell your personal information. We share data only in these limited circumstances:</p>
|
||||
<ul>
|
||||
<li><strong>Service providers:</strong> Payment processing (Stripe), email delivery, hosting infrastructure</li>
|
||||
<li><strong>Team members:</strong> If you belong to a team, other team members can see shared devices and content</li>
|
||||
<li><strong>Team members:</strong> If you belong to a team, other team members can see shared devices and content
|
||||
</li>
|
||||
<li><strong>Legal requirements:</strong> When required by law, subpoena, or court order</li>
|
||||
<li><strong>Business transfers:</strong> In the event of a merger, acquisition, or sale of assets</li>
|
||||
</ul>
|
||||
|
|
@ -97,7 +267,9 @@
|
|||
<ul>
|
||||
<li>All data stays on your servers. We have no access to it.</li>
|
||||
<li>You are the data controller and responsible for compliance with applicable privacy laws.</li>
|
||||
<li>No telemetry or usage data is sent to us from self-hosted instances. The error and diagnostic telemetry described in Section 2.5 is collected by the self-hosted server itself, not transmitted externally, and can be disabled entirely with <code>PLAYER_DEBUG_REPORTING=off</code>.</li>
|
||||
<li>No telemetry or usage data is sent to us from self-hosted instances. The error and diagnostic telemetry
|
||||
described in Section 2.5 is collected by the self-hosted server itself, not transmitted externally, and can be
|
||||
disabled entirely with <code>PLAYER_DEBUG_REPORTING=off</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Data Security</h2>
|
||||
|
|
@ -129,10 +301,12 @@
|
|||
</ul>
|
||||
|
||||
<h2>9. Children's Privacy</h2>
|
||||
<p>The Service is not intended for use by children under 13. We do not knowingly collect information from children under 13.</p>
|
||||
<p>The Service is not intended for use by children under 13. We do not knowingly collect information from children
|
||||
under 13.</p>
|
||||
|
||||
<h2>10. International Data Transfers</h2>
|
||||
<p>If you access the Service from outside the United States, your data may be transferred to and processed in the United States. By using the Service, you consent to this transfer.</p>
|
||||
<p>If you access the Service from outside the United States, your data may be transferred to and processed in the
|
||||
United States. By using the Service, you consent to this transfer.</p>
|
||||
|
||||
<h2>11. Data Retention</h2>
|
||||
<ul>
|
||||
|
|
@ -140,17 +314,20 @@
|
|||
<li>Device telemetry: 90 days (automatically pruned)</li>
|
||||
<li>Play logs: 90 days (automatically pruned)</li>
|
||||
<li>Activity logs: 90 days (automatically pruned)</li>
|
||||
<li>Error and diagnostic telemetry: rolling buffer of the 10,000 most recent entries (oldest pruned on insert)</li>
|
||||
<li>Error and diagnostic telemetry: rolling buffer of the 10,000 most recent entries (oldest pruned on insert)
|
||||
</li>
|
||||
<li>Content: retained until you delete it or your account</li>
|
||||
<li>After account deletion: all data removed within 30 days</li>
|
||||
</ul>
|
||||
|
||||
<h2>12. Changes to This Policy</h2>
|
||||
<p>We may update this policy from time to time. We will notify registered users of material changes via email. The "Last updated" date will be revised accordingly.</p>
|
||||
<p>We may update this policy from time to time. We will notify registered users of material changes via email. The
|
||||
"Last updated" date will be revised accordingly.</p>
|
||||
|
||||
<h2>13. Contact Us</h2>
|
||||
<p>For privacy-related questions or data requests, contact us at:</p>
|
||||
<p>Email: support@screentinker.com</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,23 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Terms of Service - ScreenTinker</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,sans-serif; background:#111827; color:#e2e8f0; line-height:1.8; }
|
||||
.container { max-width:800px; margin:0 auto; padding:40px 24px 80px; }
|
||||
h1 { color:#3b82f6; font-size:32px; margin-bottom:8px; }
|
||||
.updated { color:#64748b; font-size:14px; margin-bottom:40px; }
|
||||
h2 { color:#f1f5f9; font-size:20px; margin:32px 0 12px; }
|
||||
p, li { color:#94a3b8; font-size:15px; margin-bottom:12px; }
|
||||
ul { padding-left:24px; }
|
||||
a { color:#3b82f6; }
|
||||
.back { display:inline-flex; align-items:center; gap:6px; color:#64748b; font-size:13px; margin-bottom:24px; text-decoration:none; }
|
||||
.back:hover { color:#94a3b8; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 80px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #e65c00;
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.updated {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #f1f5f9;
|
||||
font-size: 20px;
|
||||
margin: 32px 0 12px;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
color: #94a3b8;
|
||||
font-size: 15px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #e65c00;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin-bottom: 24px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back">← Back to ScreenTinker</a>
|
||||
|
|
@ -25,13 +81,16 @@
|
|||
<p class="updated">Last updated: April 24, 2026</p>
|
||||
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<p>By accessing or using ScreenTinker ("the Service"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Service.</p>
|
||||
<p>By accessing or using ScreenTinker ("the Service"), you agree to be bound by these Terms of Service. If you do
|
||||
not agree, do not use the Service.</p>
|
||||
|
||||
<h2>2. Description of Service</h2>
|
||||
<p>ScreenTinker is a digital signage management platform that allows users to remotely manage content on display devices. The Service includes a web dashboard, API, Android application, web player, and related tools.</p>
|
||||
<p>ScreenTinker is a digital signage management platform that allows users to remotely manage content on display
|
||||
devices. The Service includes a web dashboard, API, Android application, web player, and related tools.</p>
|
||||
|
||||
<h2>3. Accounts</h2>
|
||||
<p>You must provide accurate information when creating an account. You are responsible for maintaining the security of your account credentials. You must notify us immediately of any unauthorized use of your account.</p>
|
||||
<p>You must provide accurate information when creating an account. You are responsible for maintaining the security
|
||||
of your account credentials. You must notify us immediately of any unauthorized use of your account.</p>
|
||||
|
||||
<h2>4. Subscription Plans and Billing</h2>
|
||||
<ul>
|
||||
|
|
@ -56,13 +115,20 @@
|
|||
</ul>
|
||||
|
||||
<h2>5.1 Prohibited Content - Zero Tolerance</h2>
|
||||
<p>ScreenTinker maintains zero tolerance for child sexual abuse material (CSAM). Any account found to upload, display, transmit, or facilitate the distribution of such material will be immediately terminated and reported to the National Center for Missing and Exploited Children (NCMEC) and appropriate law enforcement agencies, as required under 18 U.S.C. 2258A.</p>
|
||||
<p>All account data, upload records, IP logs, and device telemetry associated with such violations will be preserved and provided to law enforcement pursuant to lawful requests. This policy applies to all content uploaded, displayed, or transmitted through the Service, including content on connected devices, in the web player, and through the API.</p>
|
||||
<p>ScreenTinker maintains zero tolerance for child sexual abuse material (CSAM). Any account found to upload,
|
||||
display, transmit, or facilitate the distribution of such material will be immediately terminated and reported to
|
||||
the National Center for Missing and Exploited Children (NCMEC) and appropriate law enforcement agencies, as
|
||||
required under 18 U.S.C. 2258A.</p>
|
||||
<p>All account data, upload records, IP logs, and device telemetry associated with such violations will be preserved
|
||||
and provided to law enforcement pursuant to lawful requests. This policy applies to all content uploaded,
|
||||
displayed, or transmitted through the Service, including content on connected devices, in the web player, and
|
||||
through the API.</p>
|
||||
|
||||
<h2>6. Content</h2>
|
||||
<ul>
|
||||
<li>You retain ownership of content you upload to the Service.</li>
|
||||
<li>You grant us a limited license to store, transmit, and display your content as necessary to provide the Service.</li>
|
||||
<li>You grant us a limited license to store, transmit, and display your content as necessary to provide the
|
||||
Service.</li>
|
||||
<li>You are solely responsible for ensuring you have the rights to display any content you upload.</li>
|
||||
<li>We may remove content that violates these terms or applicable law.</li>
|
||||
</ul>
|
||||
|
|
@ -93,31 +159,49 @@
|
|||
</ul>
|
||||
|
||||
<h2>11. Intellectual Property</h2>
|
||||
<p>The ScreenTinker software, dashboard, APIs, and documentation are our intellectual property. Your subscription grants you a license to use the Service, not ownership of the software.</p>
|
||||
<p>ScreenTinker incorporates open-source software components licensed under MIT and Apache 2.0 licenses. A complete list of third-party software and their licenses is available at our <a href="/legal/third-party.html">Third-Party Software Notices</a> page.</p>
|
||||
<p>The ScreenTinker software, dashboard, APIs, and documentation are our intellectual property. Your subscription
|
||||
grants you a license to use the Service, not ownership of the software.</p>
|
||||
<p>ScreenTinker incorporates open-source software components licensed under MIT and Apache 2.0 licenses. A complete
|
||||
list of third-party software and their licenses is available at our <a href="/legal/third-party.html">Third-Party
|
||||
Software Notices</a> page.</p>
|
||||
|
||||
<h2>12. Restrictions</h2>
|
||||
<p>The core ScreenTinker software is open-source and available at <a href="https://github.com/screentinker/screentinker">github.com/screentinker/screentinker</a> under the MIT License. The restrictions in this section apply only to the hosted Service at screentinker.com and to proprietary components that are not published under the MIT License, including the license key generation and validation system, billing infrastructure, and internal deployment tooling.</p>
|
||||
<p>In connection with your use of the hosted Service and those proprietary components, you agree not to, and will not permit others to:</p>
|
||||
<p>The core ScreenTinker software is open-source and available at <a
|
||||
href="https://github.com/screentinker/screentinker">github.com/screentinker/screentinker</a> under the MIT
|
||||
License. The restrictions in this section apply only to the hosted Service at screentinker.com and to proprietary
|
||||
components that are not published under the MIT License, including the license key generation and validation
|
||||
system, billing infrastructure, and internal deployment tooling.</p>
|
||||
<p>In connection with your use of the hosted Service and those proprietary components, you agree not to, and will
|
||||
not permit others to:</p>
|
||||
<ul>
|
||||
<li>Circumvent, bypass, disable, tamper with, or forge any license keys, authentication, or usage-tracking mechanisms</li>
|
||||
<li>Circumvent, bypass, disable, tamper with, or forge any license keys, authentication, or usage-tracking
|
||||
mechanisms</li>
|
||||
<li>Share, transfer, or assign your account credentials, license keys, or API tokens to unauthorized parties</li>
|
||||
<li>Overload, attack, or otherwise abuse the hosted platform or its infrastructure</li>
|
||||
<li>Scrape, crawl, or programmatically extract data from the hosted Service beyond the provided API and documented rate limits</li>
|
||||
<li>Scrape, crawl, or programmatically extract data from the hosted Service beyond the provided API and documented
|
||||
rate limits</li>
|
||||
<li>Operate the hosted Service beyond the scope of your current subscription plan</li>
|
||||
<li>Reverse engineer, decompile, or attempt to derive the source code of proprietary components not published under the MIT License</li>
|
||||
<li>Reverse engineer, decompile, or attempt to derive the source code of proprietary components not published
|
||||
under the MIT License</li>
|
||||
</ul>
|
||||
<p>Violation of these restrictions may result in immediate termination of your account and may subject you to legal action. Nothing in this section limits, modifies, or overrides any rights granted to you by the MIT License covering the open-source portion of ScreenTinker, which governs that code exclusively.</p>
|
||||
<p>Violation of these restrictions may result in immediate termination of your account and may subject you to legal
|
||||
action. Nothing in this section limits, modifies, or overrides any rights granted to you by the MIT License
|
||||
covering the open-source portion of ScreenTinker, which governs that code exclusively.</p>
|
||||
|
||||
<h2>13. License Keys and Self-Hosted Deployments</h2>
|
||||
<ul>
|
||||
<li>Self-hosted Enterprise deployments require a valid license key issued by ScreenTinker.</li>
|
||||
<li>License keys are non-transferable and tied to a specific organization.</li>
|
||||
<li>License keys are issued for a specific term (monthly or annual) and must be renewed to maintain functionality.</li>
|
||||
<li>Upon expiration of a license key, the Software will enter a grace period of 14 days, during which functionality is preserved but a renewal notice is displayed.</li>
|
||||
<li>After the grace period, the Software will operate in a limited mode equivalent to the Free plan until a valid license key is provided.</li>
|
||||
<li>Tampering with, bypassing, or forging license keys is strictly prohibited and constitutes a material breach of these terms.</li>
|
||||
<li>We reserve the right to remotely verify license key validity and disable functionality for invalid or expired keys.</li>
|
||||
<li>License keys are issued for a specific term (monthly or annual) and must be renewed to maintain functionality.
|
||||
</li>
|
||||
<li>Upon expiration of a license key, the Software will enter a grace period of 14 days, during which
|
||||
functionality is preserved but a renewal notice is displayed.</li>
|
||||
<li>After the grace period, the Software will operate in a limited mode equivalent to the Free plan until a valid
|
||||
license key is provided.</li>
|
||||
<li>Tampering with, bypassing, or forging license keys is strictly prohibited and constitutes a material breach of
|
||||
these terms.</li>
|
||||
<li>We reserve the right to remotely verify license key validity and disable functionality for invalid or expired
|
||||
keys.</li>
|
||||
</ul>
|
||||
|
||||
<h2>14. Termination</h2>
|
||||
|
|
@ -128,18 +212,27 @@
|
|||
</ul>
|
||||
|
||||
<h2>15. Limitation of Liability</h2>
|
||||
<p>THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND. IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR USE OF THE SERVICE.</p>
|
||||
<p>THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND. IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT,
|
||||
INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR USE OF THE SERVICE.</p>
|
||||
|
||||
<h2>16. Changes to Terms</h2>
|
||||
<p>We may update these terms from time to time. We will notify registered users of material changes via email. Continued use of the Service after changes constitutes acceptance.</p>
|
||||
<p>We may update these terms from time to time. We will notify registered users of material changes via email.
|
||||
Continued use of the Service after changes constitutes acceptance.</p>
|
||||
|
||||
<h2>17. Governing Law and Jurisdiction</h2>
|
||||
<p>These Terms of Service are governed by and construed in accordance with the laws of the State of Wisconsin, United States, without regard to its conflict of law provisions. Any dispute arising out of or relating to these terms or the Service shall be resolved exclusively in the state or federal courts located in Kenosha County, Wisconsin, and you consent to the personal jurisdiction of those courts.</p>
|
||||
<p>Users accessing the Service from outside the United States acknowledge that the Service is operated from and governed by the laws of the United States and the State of Wisconsin.</p>
|
||||
<p>If any provision of these terms is held invalid or unenforceable by a court of competent jurisdiction, that provision shall be enforced to the maximum extent permissible and the remaining provisions shall remain in full force and effect.</p>
|
||||
<p>These Terms of Service are governed by and construed in accordance with the laws of the State of Wisconsin,
|
||||
United States, without regard to its conflict of law provisions. Any dispute arising out of or relating to these
|
||||
terms or the Service shall be resolved exclusively in the state or federal courts located in Kenosha County,
|
||||
Wisconsin, and you consent to the personal jurisdiction of those courts.</p>
|
||||
<p>Users accessing the Service from outside the United States acknowledge that the Service is operated from and
|
||||
governed by the laws of the United States and the State of Wisconsin.</p>
|
||||
<p>If any provision of these terms is held invalid or unenforceable by a court of competent jurisdiction, that
|
||||
provision shall be enforced to the maximum extent permissible and the remaining provisions shall remain in full
|
||||
force and effect.</p>
|
||||
|
||||
<h2>18. Contact</h2>
|
||||
<p>For questions about these terms, contact us at support@screentinker.com</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,58 +1,227 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Third-Party Licenses - ScreenTinker</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,sans-serif; background:#111827; color:#e2e8f0; line-height:1.8; }
|
||||
.container { max-width:800px; margin:0 auto; padding:40px 24px 80px; }
|
||||
h1 { color:#3b82f6; font-size:32px; margin-bottom:8px; }
|
||||
.updated { color:#64748b; font-size:14px; margin-bottom:40px; }
|
||||
h2 { color:#f1f5f9; font-size:18px; margin:32px 0 8px; }
|
||||
p, li { color:#94a3b8; font-size:14px; margin-bottom:8px; }
|
||||
a { color:#3b82f6; }
|
||||
.back { display:inline-flex; align-items:center; gap:6px; color:#64748b; font-size:13px; margin-bottom:24px; text-decoration:none; }
|
||||
.back:hover { color:#94a3b8; }
|
||||
.license-block { background:#0f172a; border:1px solid #1e293b; border-radius:8px; padding:16px; margin:12px 0 24px; font-size:12px; font-family:monospace; white-space:pre-wrap; color:#64748b; overflow-x:auto; }
|
||||
table { width:100%; border-collapse:collapse; margin:16px 0 32px; }
|
||||
th, td { padding:10px 12px; text-align:left; border-bottom:1px solid #1e293b; font-size:13px; color:#94a3b8; }
|
||||
th { color:#cbd5e1; font-weight:600; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 80px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #e65c00;
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.updated {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #f1f5f9;
|
||||
font-size: 18px;
|
||||
margin: 32px 0 8px;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #e65c00;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin-bottom: 24px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.license-block {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 12px 0 24px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
color: #64748b;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0 32px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back">← Back to ScreenTinker</a>
|
||||
<h1>Third-Party Software Notices</h1>
|
||||
<p class="updated">Last updated: March 24, 2026</p>
|
||||
|
||||
<p>ScreenTinker uses the following open-source software components. We gratefully acknowledge the contributions of these projects and their maintainers.</p>
|
||||
<p>ScreenTinker uses the following open-source software components. We gratefully acknowledge the contributions of
|
||||
these projects and their maintainers.</p>
|
||||
|
||||
<h2>Summary</h2>
|
||||
<table>
|
||||
<thead><tr><th>Package</th><th>License</th><th>Use</th></tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>License</th>
|
||||
<th>Use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Express</td><td>MIT</td><td>Web server framework</td></tr>
|
||||
<tr><td>Socket.IO</td><td>MIT</td><td>Real-time WebSocket communication</td></tr>
|
||||
<tr><td>better-sqlite3</td><td>MIT</td><td>SQLite database driver</td></tr>
|
||||
<tr><td>Multer</td><td>MIT</td><td>File upload handling</td></tr>
|
||||
<tr><td>uuid</td><td>MIT</td><td>Unique ID generation</td></tr>
|
||||
<tr><td>Sharp</td><td>Apache 2.0</td><td>Image processing and thumbnails</td></tr>
|
||||
<tr><td>cors</td><td>MIT</td><td>Cross-origin resource sharing</td></tr>
|
||||
<tr><td>bcryptjs</td><td>MIT</td><td>Password hashing</td></tr>
|
||||
<tr><td>jsonwebtoken</td><td>MIT</td><td>JWT authentication tokens</td></tr>
|
||||
<tr><td>Helmet</td><td>MIT</td><td>HTTP security headers</td></tr>
|
||||
<tr><td>google-auth-library</td><td>Apache 2.0</td><td>Google OAuth verification</td></tr>
|
||||
<tr><td>OkHttp</td><td>Apache 2.0</td><td>Android HTTP client</td></tr>
|
||||
<tr><td>Gson</td><td>Apache 2.0</td><td>Android JSON parsing</td></tr>
|
||||
<tr><td>AndroidX Media3 / ExoPlayer</td><td>Apache 2.0</td><td>Android video playback</td></tr>
|
||||
<tr><td>AndroidX libraries</td><td>Apache 2.0</td><td>Android UI and lifecycle</td></tr>
|
||||
<tr><td>Material Components for Android</td><td>Apache 2.0</td><td>Android UI components</td></tr>
|
||||
<tr><td>Socket.IO Java Client</td><td>MIT</td><td>Android WebSocket client</td></tr>
|
||||
<tr><td>Kotlin Coroutines</td><td>Apache 2.0</td><td>Android async operations</td></tr>
|
||||
<tr><td>AndroidX Security Crypto</td><td>Apache 2.0</td><td>Encrypted SharedPreferences</td></tr>
|
||||
<tr><td>AndroidX WorkManager</td><td>Apache 2.0</td><td>Background task management</td></tr>
|
||||
<tr>
|
||||
<td>Express</td>
|
||||
<td>MIT</td>
|
||||
<td>Web server framework</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket.IO</td>
|
||||
<td>MIT</td>
|
||||
<td>Real-time WebSocket communication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>better-sqlite3</td>
|
||||
<td>MIT</td>
|
||||
<td>SQLite database driver</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multer</td>
|
||||
<td>MIT</td>
|
||||
<td>File upload handling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uuid</td>
|
||||
<td>MIT</td>
|
||||
<td>Unique ID generation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sharp</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Image processing and thumbnails</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cors</td>
|
||||
<td>MIT</td>
|
||||
<td>Cross-origin resource sharing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bcryptjs</td>
|
||||
<td>MIT</td>
|
||||
<td>Password hashing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>jsonwebtoken</td>
|
||||
<td>MIT</td>
|
||||
<td>JWT authentication tokens</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Helmet</td>
|
||||
<td>MIT</td>
|
||||
<td>HTTP security headers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>google-auth-library</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Google OAuth verification</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OkHttp</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android HTTP client</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gson</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android JSON parsing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AndroidX Media3 / ExoPlayer</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android video playback</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AndroidX libraries</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android UI and lifecycle</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Material Components for Android</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android UI components</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket.IO Java Client</td>
|
||||
<td>MIT</td>
|
||||
<td>Android WebSocket client</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kotlin Coroutines</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Android async operations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AndroidX Security Crypto</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Encrypted SharedPreferences</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AndroidX WorkManager</td>
|
||||
<td>Apache 2.0</td>
|
||||
<td>Background task management</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
@ -83,7 +252,8 @@ SOFTWARE.</div>
|
|||
|
||||
<h2>Apache License 2.0</h2>
|
||||
<p>The following packages are licensed under the Apache License, Version 2.0:</p>
|
||||
<p>Sharp, google-auth-library, OkHttp, Gson, AndroidX Media3/ExoPlayer, AndroidX libraries, Material Components for Android, Kotlin Coroutines, AndroidX Security Crypto, AndroidX WorkManager</p>
|
||||
<p>Sharp, google-auth-library, OkHttp, Gson, AndroidX Media3/ExoPlayer, AndroidX libraries, Material Components for
|
||||
Android, Kotlin Coroutines, AndroidX Security Crypto, AndroidX WorkManager</p>
|
||||
<div class="license-block">Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
|
@ -101,7 +271,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.</div>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>If you have questions about the licensing of any component used in ScreenTinker, please contact us at support@screentinker.com</p>
|
||||
<p>If you have questions about the licensing of any component used in ScreenTinker, please contact us at
|
||||
support@screentinker.com</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
36
scripts/create_admin.js
Normal file
36
scripts/create_admin.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const { db } = require('../server/db/database');
|
||||
const bcrypt = require('../server/node_modules/bcryptjs');
|
||||
const { v4: uuidv4 } = require('../server/node_modules/uuid');
|
||||
|
||||
const email = 'ivandro.work@gmail.com';
|
||||
const password = '#whatever';
|
||||
const name = 'Ivandro Alves';
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const userId = uuidv4();
|
||||
|
||||
try {
|
||||
db.prepare('INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)').run(userId, email, hash, name, 'platform_admin');
|
||||
|
||||
// also create an organization and workspace for them
|
||||
const orgId = uuidv4();
|
||||
const workspaceId = uuidv4();
|
||||
|
||||
db.prepare('INSERT INTO organizations (id, name, owner_id) VALUES (?, ?, ?)').run(orgId, name + " Org", userId);
|
||||
db.prepare('INSERT INTO workspaces (id, name, organization_id) VALUES (?, ?, ?)').run(workspaceId, 'Default Workspace', orgId);
|
||||
db.prepare('INSERT INTO organization_members (organization_id, user_id, role) VALUES (?, ?, ?)').run(orgId, userId, 'admin');
|
||||
db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run(workspaceId, userId, 'admin');
|
||||
|
||||
const d = require('../server/db/database').dbOptions;
|
||||
if (d.syncUrl) db.sync();
|
||||
|
||||
console.log('Admin created successfully.');
|
||||
} catch (e) {
|
||||
if (e.message.includes('UNIQUE constraint failed')) {
|
||||
console.log('User already exists, updating password and role.');
|
||||
db.prepare('UPDATE users SET password_hash = ?, role = ? WHERE email = ?').run(hash, 'platform_admin', email);
|
||||
const d = require('../server/db/database').dbOptions;
|
||||
if (d.syncUrl) db.sync();
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
36
server/create_admin.js
Normal file
36
server/create_admin.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const { db } = require('./db/database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const email = 'ivandro.work@gmail.com';
|
||||
const password = '#whatever';
|
||||
const name = 'Ivandro Alves';
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const userId = crypto.randomUUID();
|
||||
|
||||
try {
|
||||
db.prepare('INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)').run(userId, email, hash, name, 'platform_admin');
|
||||
|
||||
// also create an organization and workspace for them
|
||||
const orgId = crypto.randomUUID();
|
||||
const workspaceId = crypto.randomUUID();
|
||||
|
||||
db.prepare('INSERT INTO organizations (id, name, owner_id) VALUES (?, ?, ?)').run(orgId, name + " Org", userId);
|
||||
db.prepare('INSERT INTO workspaces (id, name, organization_id) VALUES (?, ?, ?)').run(workspaceId, 'Default Workspace', orgId);
|
||||
db.prepare('INSERT INTO organization_members (organization_id, user_id, role) VALUES (?, ?, ?)').run(orgId, userId, 'admin');
|
||||
db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run(workspaceId, userId, 'admin');
|
||||
|
||||
const d = require('./db/database').dbOptions;
|
||||
if (d.syncUrl) db.sync();
|
||||
|
||||
console.log('Admin created successfully.');
|
||||
} catch (e) {
|
||||
if (e.message.includes('UNIQUE constraint failed')) {
|
||||
console.log('User already exists, updating password and role.');
|
||||
db.prepare('UPDATE users SET password_hash = ?, role = ? WHERE email = ?').run(hash, 'platform_admin', email);
|
||||
const d = require('./db/database').dbOptions;
|
||||
if (d.syncUrl) db.sync();
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
10
server/create_admin2.js
Normal file
10
server/create_admin2.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const { db } = require('./db/database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const email = 'ivandro.work@gmail.com';
|
||||
const password = '#whatever';
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
|
||||
db.prepare('UPDATE users SET password_hash = ?, role = ? WHERE email = ?').run(hash, 'platform_admin', email);
|
||||
if (typeof db.sync === 'function') db.sync();
|
||||
console.log('Password and role updated');
|
||||
|
|
@ -617,7 +617,7 @@ function migrateGroupSchedules() {
|
|||
recurrence_end TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
color TEXT DEFAULT '#3B82F6',
|
||||
color TEXT DEFAULT '#e65c00',
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
CHECK ((device_id IS NOT NULL AND group_id IS NULL) OR (device_id IS NULL AND group_id IS NOT NULL))
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS schedules (
|
|||
recurrence_end TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
color TEXT DEFAULT '#3B82F6',
|
||||
color TEXT DEFAULT '#e65c00',
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
CHECK ((device_id IS NOT NULL AND group_id IS NULL) OR (device_id IS NULL AND group_id IS NOT NULL))
|
||||
|
|
@ -336,7 +336,7 @@ CREATE TABLE IF NOT EXISTS device_groups (
|
|||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#3B82F6',
|
||||
color TEXT DEFAULT '#e65c00',
|
||||
playlist_id TEXT REFERENCES playlists(id) ON DELETE SET NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
|
@ -419,7 +419,7 @@ CREATE TABLE IF NOT EXISTS white_labels (
|
|||
brand_name TEXT NOT NULL DEFAULT 'ScreenTinker',
|
||||
logo_url TEXT,
|
||||
favicon_url TEXT,
|
||||
primary_color TEXT DEFAULT '#3B82F6',
|
||||
primary_color TEXT DEFAULT '#e65c00',
|
||||
secondary_color TEXT DEFAULT '#1E293B',
|
||||
bg_color TEXT DEFAULT '#111827',
|
||||
custom_domain TEXT,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ function startWalCheckpointer(db, dbPath) {
|
|||
respawnAt.length = 0;
|
||||
|
||||
// From now on the main thread NEVER inline-checkpoints (removes the loop-blocking fsync).
|
||||
db.pragma('wal_autocheckpoint = 0');
|
||||
try { db.pragma('wal_autocheckpoint = 0'); } catch (e) {
|
||||
if (!e.message.includes('Sqlite3UnsupportedStatement')) throw e;
|
||||
}
|
||||
|
||||
// Hand the worker a clean WAL (one-time, at boot; explicit checkpoints are independent of
|
||||
// wal_autocheckpoint, so this still works at 0). Also reclaims any WAL a prior crash left.
|
||||
try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch (_) { /* best-effort */ }
|
||||
|
|
|
|||
9
server/fix_branding.js
Normal file
9
server/fix_branding.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { db } = require('./db/database');
|
||||
try {
|
||||
db.prepare('DELETE FROM white_labels').run();
|
||||
const d = require('./db/database').dbOptions;
|
||||
if (d && typeof db.sync === 'function') db.sync();
|
||||
console.log('Cleared old branding overrides, server will use the new HARDCODED_BRANDING.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -21,10 +21,10 @@
|
|||
const PLATFORM_DEFAULT_ID = 'platform-default';
|
||||
|
||||
const HARDCODED_BRANDING = {
|
||||
brand_name: 'ScreenTinker',
|
||||
logo_url: null,
|
||||
favicon_url: null,
|
||||
primary_color: '#3B82F6',
|
||||
brand_name: 'GoldenSignage',
|
||||
logo_url: '/assets/logoswift.png',
|
||||
favicon_url: '/assets/pwa-icon.png',
|
||||
primary_color: '#ff6b00',
|
||||
secondary_color: '#1E293B',
|
||||
bg_color: '#111827',
|
||||
custom_css: null,
|
||||
|
|
|
|||
353
server/package-lock.json
generated
353
server/package-lock.json
generated
|
|
@ -11,7 +11,6 @@
|
|||
"@azure/msal-node": "^5.2.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.3.1",
|
||||
|
|
@ -917,17 +916,6 @@
|
|||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
|
|
@ -937,64 +925,6 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
|
|
@ -1137,12 +1067,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
|
|
@ -1451,30 +1375,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
|
@ -1598,15 +1498,6 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.9",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
|
||||
|
|
@ -1771,15 +1662,6 @@
|
|||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
|
|
@ -1903,12 +1785,6 @@
|
|||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
|
|
@ -1986,12 +1862,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
|
||||
|
|
@ -2089,12 +1959,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
|
|
@ -2290,12 +2154,6 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
|
|
@ -2679,18 +2537,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
|
|
@ -2736,12 +2582,6 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
|
@ -2767,12 +2607,6 @@
|
|||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
|
|
@ -2782,18 +2616,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.94.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
|
||||
"integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
|
@ -2889,15 +2711,6 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/otplib": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
|
||||
|
|
@ -3009,33 +2822,6 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
|
@ -3064,16 +2850,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
|
|
@ -3131,21 +2907,6 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
|
|
@ -3437,51 +3198,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
|
|
@ -3775,15 +3491,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "20.4.1",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.4.1.tgz",
|
||||
|
|
@ -3801,48 +3508,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
||||
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
|
||||
|
|
@ -3897,18 +3562,6 @@
|
|||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
|
@ -4123,12 +3776,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
"@azure/msal-node": "^5.2.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.3.1",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
|
@ -118,35 +119,138 @@
|
|||
<script src="/player/debug-overlay.js" defer></script>
|
||||
<title>ScreenTinker Player</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: -apple-system, sans-serif; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Setup Screen */
|
||||
#setupScreen {
|
||||
position: fixed; inset: 0; background: #111827; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; z-index: 1000; color: #f1f5f9;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #111827;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
#setupScreen h1 {
|
||||
font-size: 36px;
|
||||
color: #e65c00;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#setupScreen .subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
#setupScreen .form {
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
#setupScreen label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#setupScreen input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
color: #f1f5f9;
|
||||
font-size: 16px;
|
||||
margin-bottom: 24px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#setupScreen input:focus {
|
||||
border-color: #e65c00;
|
||||
}
|
||||
|
||||
#setupScreen button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #e65c00;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#setupScreen button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
#setupScreen button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pairing-code {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #e65c00;
|
||||
font-family: monospace;
|
||||
letter-spacing: 12px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.pairing-hint {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-msg {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #334155;
|
||||
border-top-color: #e65c00;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 24px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
#setupScreen h1 { font-size: 36px; color: #3b82f6; margin-bottom: 8px; }
|
||||
#setupScreen .subtitle { color: #94a3b8; font-size: 16px; margin-bottom: 48px; }
|
||||
#setupScreen .form { width: 400px; max-width: 90vw; }
|
||||
#setupScreen label { display: block; font-size: 14px; color: #94a3b8; margin-bottom: 8px; }
|
||||
#setupScreen input { width: 100%; padding: 12px; background: #0f172a; border: 1px solid #334155;
|
||||
border-radius: 8px; color: #f1f5f9; font-size: 16px; margin-bottom: 24px; outline: none; }
|
||||
#setupScreen input:focus { border-color: #3b82f6; }
|
||||
#setupScreen button { width: 100%; padding: 12px; background: #3b82f6; color: white;
|
||||
border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; }
|
||||
#setupScreen button:hover { background: #2563eb; }
|
||||
#setupScreen button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.pairing-code { font-size: 72px; font-weight: 700; color: #3b82f6; font-family: monospace;
|
||||
letter-spacing: 12px; margin: 24px 0; }
|
||||
.pairing-hint { color: #64748b; font-size: 14px; }
|
||||
.status-msg { color: #94a3b8; font-size: 14px; margin-top: 16px; }
|
||||
.spinner { width: 40px; height: 40px; border: 3px solid #334155; border-top-color: #3b82f6;
|
||||
border-radius: 50%; animation: spin 1s linear infinite; margin: 24px auto; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Player */
|
||||
#playerContainer { position: fixed; inset: 0; background: #000; }
|
||||
#playerContainer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* Fullscreen single-zone playback: YouTube's IFrame API measures the placeholder
|
||||
at construction time. If that happens before layout settles, YT bakes in a
|
||||
300x150 fallback as inline pixel dimensions on the iframe, which our %-based
|
||||
|
|
@ -154,14 +258,36 @@
|
|||
#playerContainer>iframe,
|
||||
#playerContainer>div>iframe {
|
||||
position: absolute !important;
|
||||
top: 0 !important; left: 0 !important;
|
||||
width: 100% !important; height: 100% !important;
|
||||
border: none !important; display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.zone {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zone video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.zone img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.zone iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.zone { position: absolute; overflow: hidden; }
|
||||
.zone video { width: 100%; height: 100%; object-fit: cover; }
|
||||
.zone img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.zone iframe { width: 100%; height: 100%; border: none; }
|
||||
|
||||
/* Video wall mode.
|
||||
wall-stage maps the wall's player_rect into this device's viewport
|
||||
|
|
@ -171,33 +297,95 @@
|
|||
which keeps vertical position identical between devices that share
|
||||
a viewport height — without that, cover-cropping on different stage
|
||||
aspects (different innerWidths) shifts content vertically. */
|
||||
#playerContainer.wall-mode { overflow: hidden; background: #000; }
|
||||
.wall-stage { position: absolute; }
|
||||
#playerContainer.wall-mode {
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.wall-stage {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.wall-stage>video,
|
||||
.wall-stage > img { width: 100%; height: 100%; object-fit: fill; display: block; }
|
||||
.wall-stage > iframe { width: 100%; height: 100%; border: none; display: block; }
|
||||
.wall-stage>img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wall-stage>iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wall-mode #playerContainer>iframe,
|
||||
.wall-mode #playerContainer > div > iframe { position: static !important; width: 100% !important; height: 100% !important; }
|
||||
.wall-mode #playerContainer>div>iframe {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* #109: PiP overlay layer — a fixed full-viewport layer ABOVE #playerContainer that
|
||||
the playlist never touches. The same orientation transform is applied to it as to
|
||||
#playerContainer, so corner positions track the visible content in every orientation.
|
||||
Pointer-transparent and empty (invisible) until an overlay is shown. */
|
||||
#pipContainer { position: fixed; inset: 0; pointer-events: none; z-index: 9000; }
|
||||
#pipContainer > .pip-box { position: absolute; overflow: hidden; box-sizing: border-box; box-shadow: 0 6px 28px rgba(0,0,0,0.55); }
|
||||
#pipContainer > .pip-box > .pip-title { font: 600 16px sans-serif; padding: 6px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#pipContainer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9000;
|
||||
}
|
||||
|
||||
#pipContainer>.pip-box {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
#pipContainer>.pip-box>.pip-title {
|
||||
font: 600 16px sans-serif;
|
||||
padding: 6px 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#pipContainer>.pip-box>img,
|
||||
#pipContainer > .pip-box > iframe { display: block; width: 100%; border: 0; }
|
||||
#pipContainer>.pip-box>iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Status overlay */
|
||||
#statusOverlay {
|
||||
position: fixed; inset: 0; background: #000; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; color: #94a3b8; z-index: 500;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #94a3b8;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
#statusOverlay h2 {
|
||||
color: #e65c00;
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#statusOverlay p {
|
||||
font-size: 16px;
|
||||
}
|
||||
#statusOverlay h2 { color: #3b82f6; font-size: 28px; margin-bottom: 8px; }
|
||||
#statusOverlay p { font-size: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Setup Screen -->
|
||||
<div id="setupScreen">
|
||||
|
|
@ -388,8 +576,10 @@
|
|||
if (!config || !config.deviceId || !config.deviceToken) return; // unpaired -> nothing to attribute
|
||||
__exitSent = true;
|
||||
const url = (config.serverUrl || window.location.origin) + '/api/device/exit';
|
||||
const body = JSON.stringify({ device_id: config.deviceId, device_token: config.deviceToken,
|
||||
reason, detail: (typeof detail === 'string' && detail) ? detail.slice(0, 200) : undefined });
|
||||
const body = JSON.stringify({
|
||||
device_id: config.deviceId, device_token: config.deviceToken,
|
||||
reason, detail: (typeof detail === 'string' && detail) ? detail.slice(0, 200) : undefined
|
||||
});
|
||||
const blob = new Blob([body], { type: 'application/json' }); // Content-Type so express.json parses it
|
||||
if (navigator.sendBeacon && navigator.sendBeacon(url, blob)) return;
|
||||
fetch(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' }, keepalive: true }).catch(() => { });
|
||||
|
|
@ -695,7 +885,7 @@
|
|||
const tapOverlay = document.createElement('div');
|
||||
tapOverlay.style.cssText = 'position:fixed;inset:0;background:#111827;z-index:2000;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer';
|
||||
tapOverlay.innerHTML = `
|
||||
<h1 style="color:#3b82f6;font-size:36px;font-family:sans-serif;margin-bottom:12px">ScreenTinker</h1>
|
||||
<h1 style="color:#e65c00;font-size:36px;font-family:sans-serif;margin-bottom:12px">ScreenTinker</h1>
|
||||
<p style="color:#94a3b8;font-size:18px;font-family:sans-serif">Tap anywhere to start</p>
|
||||
<p style="color:#64748b;font-size:13px;font-family:sans-serif;margin-top:24px">Audio requires user interaction</p>
|
||||
`;
|
||||
|
|
@ -2947,7 +3137,7 @@
|
|||
if (!captured) {
|
||||
ctx.fillStyle = '#111827';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#3b82f6';
|
||||
ctx.fillStyle = '#e65c00';
|
||||
ctx.font = 'bold 28px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('ScreenTinker Web Player', W / 2, H / 2 - 40);
|
||||
|
|
@ -3023,7 +3213,7 @@
|
|||
infoDiv.id = 'infoOverlay';
|
||||
infoDiv.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:800;display:none;flex-direction:column;align-items:center;justify-content:center;color:#f1f5f9;font-family:-apple-system,sans-serif';
|
||||
infoDiv.innerHTML = `
|
||||
<h2 style="color:#3b82f6;margin-bottom:16px">${_t('info_title')}</h2>
|
||||
<h2 style="color:#e65c00;margin-bottom:16px">${_t('info_title')}</h2>
|
||||
<div style="font-size:14px;line-height:2;text-align:center;color:#94a3b8" id="infoContent"></div>
|
||||
<p style="margin-top:24px;font-size:12px;color:#64748b">${_t('info_close_hint')}</p>
|
||||
`;
|
||||
|
|
@ -3144,4 +3334,5 @@
|
|||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -351,7 +351,7 @@ router.put('/branding', requirePlatformAdmin, (req, res) => {
|
|||
PLATFORM_DEFAULT_ID, req.user.id,
|
||||
req.body.brand_name || 'ScreenTinker',
|
||||
req.body.logo_url || null, req.body.favicon_url || null,
|
||||
req.body.primary_color || '#3B82F6', req.body.secondary_color || '#1E293B', req.body.bg_color || '#111827',
|
||||
req.body.primary_color || '#e65c00', req.body.secondary_color || '#1E293B', req.body.bg_color || '#111827',
|
||||
req.body.custom_css || null, req.body.hide_branding ? 1 : 0
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function normalizeDesign(raw) {
|
|||
x: Math.min(clampN(e.x, 0, 100, 0), 100 - w),
|
||||
y: Math.min(clampN(e.y, 0, 100, 0), 100 - h),
|
||||
width: w, height: h,
|
||||
color: hex(e.color, '#3b82f6'), opacity: clampN(e.opacity, 0, 1, 0.85), radius: 0,
|
||||
color: hex(e.color, '#e65c00'), opacity: clampN(e.opacity, 0, 1, 0.85), radius: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ router.post('/', (req, res) => {
|
|||
if (color && !VALID_COLOR.test(color)) return res.status(400).json({ error: 'invalid color format, use #RRGGBB' });
|
||||
const id = uuidv4();
|
||||
db.prepare('INSERT INTO device_groups (id, user_id, workspace_id, name, color) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(id, req.user.id, req.workspaceId, name, color || '#3B82F6');
|
||||
.run(id, req.user.id, req.workspaceId, name, color || '#e65c00');
|
||||
res.status(201).json(db.prepare('SELECT * FROM device_groups WHERE id = ?').get(id));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ router.get('/:id/render', (req, res) => {
|
|||
text-align:center; cursor:pointer; transition:all 0.2s ease; touch-action:manipulation;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px;
|
||||
}
|
||||
.kiosk-btn:hover, .kiosk-btn:active { background:${safeColor(style.buttonHover, '#3b82f6')}; border-color:${safeColor(style.buttonHover, '#3b82f6')}; transform:scale(1.02); }
|
||||
.kiosk-btn:hover, .kiosk-btn:active { background:${safeColor(style.buttonHover, '#e65c00')}; border-color:${safeColor(style.buttonHover, '#e65c00')}; transform:scale(1.02); }
|
||||
.kiosk-btn .icon { font-size:${safeNumber(style.iconSize, 48)}px; }
|
||||
.kiosk-btn .label { font-size:${safeNumber(style.labelSize, 20)}px; font-weight:600; }
|
||||
.kiosk-btn .sublabel { font-size:${safeNumber(style.sublabelSize, 14)}px; opacity:0.6; }
|
||||
|
|
@ -245,7 +245,7 @@ function getDefaultKioskConfig() {
|
|||
columns: 3,
|
||||
buttonBg: '#1e293b',
|
||||
buttonBorder: '#334155',
|
||||
buttonHover: '#3b82f6',
|
||||
buttonHover: '#e65c00',
|
||||
buttonRadius: 16,
|
||||
buttonPadding: 32,
|
||||
gap: 24,
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ router.post('/', (req, res) => {
|
|||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, req.user.id, targetWorkspaceId, device_id || null, group_id || null, zone_id || null, content_id || null, widget_id || null,
|
||||
layout_id || null, playlist_id || null, title || '', start_time, end_time, timezone || 'UTC',
|
||||
recurrence || null, recurrence_end || null, priority || 0, color || '#3B82F6');
|
||||
recurrence || null, recurrence_end || null, priority || 0, color || '#e65c00');
|
||||
|
||||
const schedule = db.prepare('SELECT * FROM schedules WHERE id = ?').get(id);
|
||||
res.status(201).json(schedule);
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ router.post('/import', importUpload.single('file'), async (req, res) => {
|
|||
if (!devId && !grpId) continue;
|
||||
const newId = uuid.v4();
|
||||
const playlistId = s.playlist_id ? (idMap.playlists[s.playlist_id] || null) : null;
|
||||
db.prepare(`INSERT INTO schedules (id, user_id, device_id, group_id, zone_id, content_id, widget_id, layout_id, playlist_id, title, start_time, end_time, timezone, recurrence, recurrence_end, priority, enabled, color, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newId, userId, devId, grpId, s.zone_id ? (idMap.zones[s.zone_id] || null) : null, s.content_id ? (idMap.content[s.content_id] || null) : null, s.widget_id ? (idMap.widgets[s.widget_id] || null) : null, s.layout_id ? (idMap.layouts[s.layout_id] || null) : null, playlistId, s.title || '', s.start_time, s.end_time, s.timezone || 'UTC', s.recurrence || null, s.recurrence_end || null, s.priority || 0, s.enabled !== undefined ? s.enabled : 1, s.color || '#3B82F6', s.created_at || Math.floor(Date.now() / 1000));
|
||||
db.prepare(`INSERT INTO schedules (id, user_id, device_id, group_id, zone_id, content_id, widget_id, layout_id, playlist_id, title, start_time, end_time, timezone, recurrence, recurrence_end, priority, enabled, color, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newId, userId, devId, grpId, s.zone_id ? (idMap.zones[s.zone_id] || null) : null, s.content_id ? (idMap.content[s.content_id] || null) : null, s.widget_id ? (idMap.widgets[s.widget_id] || null) : null, s.layout_id ? (idMap.layouts[s.layout_id] || null) : null, playlistId, s.title || '', s.start_time, s.end_time, s.timezone || 'UTC', s.recurrence || null, s.recurrence_end || null, s.priority || 0, s.enabled !== undefined ? s.enabled : 1, s.color || '#e65c00', s.created_at || Math.floor(Date.now() / 1000));
|
||||
stats.schedules++;
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +446,7 @@ router.post('/import', importUpload.single('file'), async (req, res) => {
|
|||
for (const g of (data.device_groups || [])) {
|
||||
const newId = uuid.v4();
|
||||
idMap.groups[g.id] = newId;
|
||||
db.prepare(`INSERT INTO device_groups (id, user_id, workspace_id, name, color, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(newId, userId, workspaceId, g.name, g.color || '#3B82F6', g.created_at || Math.floor(Date.now() / 1000));
|
||||
db.prepare(`INSERT INTO device_groups (id, user_id, workspace_id, name, color, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(newId, userId, workspaceId, g.name, g.color || '#e65c00', g.created_at || Math.floor(Date.now() / 1000));
|
||||
stats.device_groups++;
|
||||
}
|
||||
for (const gm of (data.device_group_members || [])) {
|
||||
|
|
@ -468,9 +468,9 @@ router.post('/import', importUpload.single('file'), async (req, res) => {
|
|||
const wl = data.white_label;
|
||||
const existing = db.prepare('SELECT id FROM white_labels WHERE workspace_id = ?').get(workspaceId);
|
||||
if (existing) {
|
||||
db.prepare(`UPDATE white_labels SET brand_name=?, logo_url=?, favicon_url=?, primary_color=?, bg_color=?, custom_domain=?, custom_css=?, hide_branding=?, updated_at=strftime('%s','now') WHERE workspace_id=?`).run(wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0, workspaceId);
|
||||
db.prepare(`UPDATE white_labels SET brand_name=?, logo_url=?, favicon_url=?, primary_color=?, bg_color=?, custom_domain=?, custom_css=?, hide_branding=?, updated_at=strftime('%s','now') WHERE workspace_id=?`).run(wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#e65c00', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0, workspaceId);
|
||||
} else {
|
||||
db.prepare(`INSERT INTO white_labels (id, user_id, workspace_id, brand_name, logo_url, favicon_url, primary_color, bg_color, custom_domain, custom_css, hide_branding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid.v4(), userId, workspaceId, wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0);
|
||||
db.prepare(`INSERT INTO white_labels (id, user_id, workspace_id, brand_name, logo_url, favicon_url, primary_color, bg_color, custom_domain, custom_css, hide_branding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid.v4(), userId, workspaceId, wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#e65c00', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ router.post('/', requireWorkspaceAdmin, (req, res) => {
|
|||
db.prepare(`INSERT INTO white_labels (id, user_id, workspace_id, brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color, custom_domain, custom_css, hide_branding)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
id, req.user.id, req.workspaceId, brand_name || 'ScreenTinker', logo_url || null, favicon_url || null,
|
||||
primary_color || '#3B82F6', secondary_color || '#1E293B', bg_color || '#111827',
|
||||
primary_color || '#e65c00', secondary_color || '#1E293B', bg_color || '#111827',
|
||||
custom_domain || null, custom_css || null, hide_branding ? 1 : 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -249,13 +249,15 @@ app.get(['/integrations', '/integrations/'], (req, res) => {
|
|||
// Serve frontend static files
|
||||
// JS/CSS/HTML: no-cache (always revalidate, uses ETag/304)
|
||||
// Images/fonts/icons: long cache for Cloudflare + browser
|
||||
app.use(express.static(config.frontendDir, { index: false, etag: true, lastModified: true, setHeaders: (res, filePath) => {
|
||||
app.use(express.static(config.frontendDir, {
|
||||
index: false, etag: true, lastModified: true, setHeaders: (res, filePath) => {
|
||||
if (filePath.endsWith('.js') || filePath.endsWith('.css') || filePath.endsWith('.html')) {
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
} else if (/\.(png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|mp4|webm)$/i.test(filePath)) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=2592000'); // 30 days
|
||||
}
|
||||
}}));
|
||||
}
|
||||
}));
|
||||
|
||||
// Player HTML: dynamic route. Injects a small inline window.__playerConfig
|
||||
// script before the debug-overlay.js tag so the client knows whether to send
|
||||
|
|
@ -312,11 +314,13 @@ app.get('/player/transitions.js', (req, res) => {
|
|||
// Serve web player at /player (same no-cache for JS/HTML). The index.html
|
||||
// route above intercepts the HTML requests; everything else still falls
|
||||
// through to this static handler (debug-overlay.js, sw.js, manifest, etc).
|
||||
app.use('/player', express.static(path.join(__dirname, 'player'), { etag: true, lastModified: true, setHeaders: (res, filePath) => {
|
||||
app.use('/player', express.static(path.join(__dirname, 'player'), {
|
||||
etag: true, lastModified: true, setHeaders: (res, filePath) => {
|
||||
if (filePath.endsWith('.js') || filePath.endsWith('.css') || filePath.endsWith('.html')) {
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
}
|
||||
}}));
|
||||
}
|
||||
}));
|
||||
|
||||
// Serve setup scripts
|
||||
app.use('/scripts', express.static(path.join(__dirname, '..', 'scripts')));
|
||||
|
|
@ -939,7 +943,7 @@ const otaDownloadState = otaDownloadGuard.prodState(); // #146 P3.8: shared si
|
|||
app.get('/download/apk', (req, res) => {
|
||||
const apk = apkCache.get();
|
||||
if (!apk.exists) {
|
||||
return res.status(404).send(`<!DOCTYPE html><html><head><title>APK Not Found</title><style>body{font-family:-apple-system,system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}div{text-align:center;max-width:500px;padding:24px}h1{color:#f87171;font-size:24px}code{background:#1e293b;padding:2px 8px;border-radius:4px;font-size:14px}p{line-height:1.6;color:#94a3b8}</style></head><body><div><h1>APK Not Available</h1><p>The Android APK has not been compiled yet. In Docker, mount a built APK at <code>/data/ScreenTinker.apk</code>, or use the <a href="/player" style="color:#3b82f6">web player</a>.</p></div></body></html>`);
|
||||
return res.status(404).send(`<!DOCTYPE html><html><head><title>APK Not Found</title><style>body{font-family:-apple-system,system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}div{text-align:center;max-width:500px;padding:24px}h1{color:#f87171;font-size:24px}code{background:#1e293b;padding:2px 8px;border-radius:4px;font-size:14px}p{line-height:1.6;color:#94a3b8}</style></head><body><div><h1>APK Not Available</h1><p>The Android APK has not been compiled yet. In Docker, mount a built APK at <code>/data/ScreenTinker.apk</code>, or use the <a href="/player" style="color:#e65c00">web player</a>.</p></div></body></html>`);
|
||||
}
|
||||
|
||||
const verdict = otaDownloadGuard.admit(otaDownloadState, getBand());
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ async function checkOfflineDevices(io) {
|
|||
// previously inlined in the webhook payload at sendEmailAlert.
|
||||
function buildAlertHtml(recipientName, subject, body) {
|
||||
return `<div style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
||||
<h2 style="color:#3b82f6">ScreenTinker Alert</h2>
|
||||
<h2 style="color:#e65c00">ScreenTinker Alert</h2>
|
||||
<p>Hi ${escapeHtml(recipientName || 'there')},</p>
|
||||
<div style="background:#f1f5f9;padding:16px;border-radius:8px;margin:16px 0">
|
||||
<strong>${escapeHtml(subject)}</strong><br><br>
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ const ai = require('../routes/ai');
|
|||
const { normalizeDesign, endpointAllowed } = ai;
|
||||
|
||||
test('normalizeDesign: keeps valid text+shape, sets background', () => {
|
||||
const d = normalizeDesign({ background: '#102030', elements: [
|
||||
const d = normalizeDesign({
|
||||
background: '#102030', elements: [
|
||||
{ type: 'text', x: 5, y: 5, text: 'HELLO', fontSize: 90, color: '#ffffff', bold: true },
|
||||
{ type: 'shape', x: 0, y: 90, width: 100, height: 8, color: '#ff0000', opacity: 0.5 },
|
||||
]});
|
||||
]
|
||||
});
|
||||
assert.equal(d.background, '#102030');
|
||||
assert.equal(d.elements.length, 2);
|
||||
const txt = d.elements.find((e) => e.type === 'text');
|
||||
|
|
@ -32,25 +34,29 @@ test('normalizeDesign: keeps valid text+shape, sets background', () => {
|
|||
});
|
||||
|
||||
test('normalizeDesign: converts pixel shape dims to %, clamps ranges', () => {
|
||||
const d = normalizeDesign({ elements: [
|
||||
const d = normalizeDesign({
|
||||
elements: [
|
||||
{ type: 'shape', x: -10, y: 200, width: 1920, height: 1080, color: 'red', opacity: 5 },
|
||||
]});
|
||||
]
|
||||
});
|
||||
const s = d.elements[0];
|
||||
assert.equal(s.x, 0, 'x clamped so full-width shape fits');
|
||||
assert.equal(s.y, 0, 'y clamped so full-height shape fits (y+height<=100)');
|
||||
assert.ok(Math.abs(s.width - 100) < 0.01, '1920px -> 100%');
|
||||
assert.ok(Math.abs(s.height - 100) < 0.01, '1080px -> 100%');
|
||||
assert.equal(s.color, '#3b82f6', 'non-hex color -> default');
|
||||
assert.equal(s.color, '#e65c00', 'non-hex color -> default');
|
||||
assert.equal(s.opacity, 1, 'opacity clamped to 1');
|
||||
});
|
||||
|
||||
test('normalizeDesign: strips HTML from text, drops empty/invalid', () => {
|
||||
const d = normalizeDesign({ elements: [
|
||||
const d = normalizeDesign({
|
||||
elements: [
|
||||
{ type: 'text', text: '<img src=x onerror=alert(1)>Sale</b>', fontSize: 9999 },
|
||||
{ type: 'text', text: ' ' },
|
||||
{ type: 'bogus', text: 'x' },
|
||||
null,
|
||||
]});
|
||||
]
|
||||
});
|
||||
assert.equal(d.elements.length, 1, 'only the one real text survives');
|
||||
assert.equal(d.elements[0].text, 'Sale');
|
||||
assert.ok(!/[<>]/.test(d.elements[0].text), 'no angle brackets');
|
||||
|
|
@ -77,9 +83,11 @@ test('endpointAllowed: blocks private/internal when hosted, allows public https'
|
|||
});
|
||||
|
||||
test('normalizeDesign: long/large text is shrunk + repositioned to fit canvas', () => {
|
||||
const d = normalizeDesign({ elements: [
|
||||
const d = normalizeDesign({
|
||||
elements: [
|
||||
{ type: 'text', x: 30, y: 95, text: 'GRAND OPENING THIS FRIDAY EVERYONE WELCOME', fontSize: 160, color: '#fff' },
|
||||
]});
|
||||
]
|
||||
});
|
||||
const e = d.elements[0];
|
||||
const w = e.text.length * e.fontSize * 0.075;
|
||||
assert.ok(e.x + w <= 96.5, `fits horizontally (x=${e.x} w=${w.toFixed(1)})`);
|
||||
|
|
@ -89,11 +97,13 @@ test('normalizeDesign: long/large text is shrunk + repositioned to fit canvas',
|
|||
});
|
||||
|
||||
test('normalizeDesign: separates overlapping text + orders shapes behind text', () => {
|
||||
const d = normalizeDesign({ elements: [
|
||||
const d = normalizeDesign({
|
||||
elements: [
|
||||
{ type: 'text', x: 5, y: 40, text: 'HEADLINE TEXT HERE', fontSize: 60, color: '#fff' },
|
||||
{ type: 'text', x: 5, y: 41, text: 'SUBTEXT OVERLAPPING IT', fontSize: 40, color: '#fff' },
|
||||
{ type: 'shape', x: 0, y: 0, width: 100, height: 100, color: '#000', opacity: 0.5 },
|
||||
]});
|
||||
]
|
||||
});
|
||||
assert.equal(d.elements[0].type, 'shape', 'shape rendered behind (first in array)');
|
||||
const texts = d.elements.filter((e) => e.type === 'text');
|
||||
const hi = texts[0].y <= texts[1].y ? texts[0] : texts[1];
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ db.exec(`
|
|||
);
|
||||
CREATE TABLE white_labels (
|
||||
id TEXT PRIMARY KEY, user_id TEXT, brand_name TEXT NOT NULL DEFAULT 'ScreenTinker',
|
||||
logo_url TEXT, favicon_url TEXT, primary_color TEXT DEFAULT '#3B82F6',
|
||||
logo_url TEXT, favicon_url TEXT, primary_color TEXT DEFAULT '#e65c00',
|
||||
secondary_color TEXT DEFAULT '#1E293B', bg_color TEXT DEFAULT '#111827',
|
||||
custom_domain TEXT, custom_css TEXT, hide_branding INTEGER DEFAULT 0,
|
||||
workspace_id TEXT, created_at INTEGER DEFAULT (strftime('%s','now')), updated_at INTEGER DEFAULT (strftime('%s','now'))
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ test('text widget: raw HTML is isolated in a null-origin sandboxed iframe', asyn
|
|||
});
|
||||
|
||||
test('valid color/gradient backgrounds are preserved', async () => {
|
||||
seed('clock2', 'clock', { background: 'linear-gradient(45deg, #ff0000, #00ff00)', color: '#3B82F6' });
|
||||
seed('clock2', 'clock', { background: 'linear-gradient(45deg, #ff0000, #00ff00)', color: '#e65c00' });
|
||||
const html = await render('clock2');
|
||||
assert.ok(html.includes('linear-gradient(45deg, #ff0000, #00ff00)'), 'legit gradient preserved');
|
||||
assert.ok(html.includes('color:#3B82F6'), 'legit hex color preserved');
|
||||
assert.ok(html.includes('color:#e65c00'), 'legit hex color preserved');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,34 @@
|
|||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
background: #000; color: #f1f5f9;
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
color: #f1f5f9;
|
||||
font-family: "Samsung One", "Tizen Sans", Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.screen {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Setup / pairing card */
|
||||
.card {
|
||||
|
|
@ -24,66 +39,161 @@ html, body {
|
|||
text-align: center;
|
||||
max-width: 760px;
|
||||
}
|
||||
.card h1 { color: #3b82f6; font-size: 44px; margin-bottom: 6px; }
|
||||
.sub { color: #94a3b8; font-size: 22px; margin-bottom: 36px; }
|
||||
.card label { display: block; text-align: left; color: #94a3b8; font-size: 18px; margin-bottom: 8px; }
|
||||
|
||||
.card h1 {
|
||||
color: #e65c00;
|
||||
font-size: 44px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: #94a3b8;
|
||||
font-size: 22px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.card label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
color: #94a3b8;
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#serverUrl {
|
||||
width: 100%; font-size: 26px; padding: 16px 20px;
|
||||
border-radius: 10px; border: 2px solid #334155;
|
||||
background: #0b1220; color: #f1f5f9; margin-bottom: 24px;
|
||||
width: 100%;
|
||||
font-size: 26px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #334155;
|
||||
background: #0b1220;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#serverUrl:focus {
|
||||
outline: none;
|
||||
border-color: #e65c00;
|
||||
}
|
||||
#serverUrl:focus { outline: none; border-color: #3b82f6; }
|
||||
|
||||
button {
|
||||
font-size: 24px; font-weight: bold; color: #fff;
|
||||
background: #3b82f6; border: none; border-radius: 10px;
|
||||
padding: 16px 40px; cursor: pointer;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: #e65c00;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 16px 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:focus { outline: 3px solid #93c5fd; }
|
||||
button.ghost { background: transparent; color: #64748b; font-size: 18px; margin-top: 24px; padding: 8px; }
|
||||
|
||||
.status { color: #64748b; font-size: 18px; margin-top: 20px; min-height: 24px; }
|
||||
.status.error { color: #ef4444; }
|
||||
button:focus {
|
||||
outline: 3px solid #93c5fd;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 18px;
|
||||
margin-top: 24px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #64748b;
|
||||
font-size: 18px;
|
||||
margin-top: 20px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Pairing code */
|
||||
.code {
|
||||
font-size: 96px; font-weight: bold; letter-spacing: 16px;
|
||||
color: #22c55e; margin: 24px 0; font-family: monospace;
|
||||
font-size: 96px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 16px;
|
||||
color: #22c55e;
|
||||
margin: 24px 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #94a3b8;
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.hint { color: #94a3b8; font-size: 20px; line-height: 1.5; }
|
||||
|
||||
/* Playback stage */
|
||||
.stage { background: #000; }
|
||||
.stage img, .stage video, .stage iframe {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%; border: 0;
|
||||
.stage {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.stage img,
|
||||
.stage video,
|
||||
.stage iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.stage img.contain,
|
||||
.stage video.contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stage img.cover,
|
||||
.stage video.cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.stage img.fill,
|
||||
.stage video.fill {
|
||||
object-fit: fill;
|
||||
}
|
||||
.stage img.contain, .stage video.contain { object-fit: contain; }
|
||||
.stage img.cover, .stage video.cover { object-fit: cover; }
|
||||
.stage img.fill, .stage video.fill { object-fit: fill; }
|
||||
|
||||
/* Video wall mode: the stage is positioned (in vw/vh) as this screen's slice of the
|
||||
wall's player rect; media stretches to fill (object-fit:fill) so a given source row
|
||||
lands on the same physical line across every screen that shares a viewport height. */
|
||||
.stage.wall-mode img, .stage.wall-mode video, .stage.wall-mode iframe { object-fit: fill; }
|
||||
.stage.wall-mode img,
|
||||
.stage.wall-mode video,
|
||||
.stage.wall-mode iframe {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
/* #109: PiP overlay layer. Sits above #stage and fills the same viewport box so a
|
||||
child positioned to a corner lands in the corner of the visible content. app.js
|
||||
applies the SAME orientation transform here as on #stage (portrait/flipped). It is
|
||||
pointer-transparent and empty (invisible) until PipOverlay renders into it. */
|
||||
#pip {
|
||||
position: fixed; top: 0; left: 0;
|
||||
width: 100vw; height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
#pip > div { box-shadow: 0 6px 28px rgba(0,0,0,0.55); }
|
||||
|
||||
#pip>div {
|
||||
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
.toast {
|
||||
position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||
background: rgba(17,24,39,0.92); color: #f1f5f9;
|
||||
padding: 12px 24px; border-radius: 10px; font-size: 18px;
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(17, 24, 39, 0.92);
|
||||
color: #f1f5f9;
|
||||
padding: 12px 24px;
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
|
@ -546,7 +546,7 @@
|
|||
}
|
||||
if (!captured) {
|
||||
ctx.fillStyle = '#111827'; ctx.fillRect(0, 0, 960, 540);
|
||||
ctx.fillStyle = '#3b82f6'; ctx.font = 'bold 28px sans-serif'; ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#e65c00'; ctx.font = 'bold 28px sans-serif'; ctx.textAlign = 'center';
|
||||
ctx.fillText('ScreenTinker (Tizen)', 480, 235);
|
||||
ctx.fillStyle = '#94a3b8'; ctx.font = '16px sans-serif';
|
||||
ctx.fillText('Live preview unavailable for video / YouTube on Tizen', 480, 280);
|
||||
|
|
|
|||
Loading…
Reference in a new issue