Tuxifan commited on
Commit
8876673
·
verified ·
1 Parent(s): c44db84

CROW_ROUTE(app, "/")([]() {

Browse files

crow::response fres;
fres.redirect_perm("/static");
return fres;
});

// Gets JSON array of recoded videos, included currently recording video
CROW_ROUTE(app, "/videos")([&rec]() {
json fres = json::array({});
for (const auto entry : rec.recordings())
if (entry.is_regular_file())
fres.emplace_back(entry.path().filename().string());
return crow::response("application/json", fres.dump());
});

// Get duration of current recording
CROW_ROUTE(app, "/duration")([&rec](const crow::request& req) {
const auto fres = rec.uptime();
if (fres.has_value())
return crow::response("text/plain", std::to_string(fres.value()));
return crow::response(crow::status::NOT_FOUND);
});

// Gets thumbnail of current recording
CROW_ROUTE(app, "/thumbnail")([&rec](const crow::request& req) {
if (!rec.running())
return crow::response(crow::status::NOT_FOUND);
crow::response fres;
fres.redirect("/thumbnail/" + rec.file().filename().string());
return fres;
});

// Gets thumbnail of specified recording
CROW_ROUTE(app, "/thumbnail/<string>")([&rec](const crow::request& req, std::string_view filename) {
TemporaryFile *tf;

// Try cache first
{
auto fres = thumbnailCache.find(filename);
if (!(rec.running() && filename == rec.file().filename().string()) && fres != thumbnailCache.end()) {
tf = &fres->second;
} else {
VideoLastFrameGrabber lsfg(rec.file(filename));
tf = &thumbnailCache.emplace(filename, TemporaryFile()).first->second;
lsfg.grabToFile(tf->getPath(), VideoLastFrameGrabber::PNG);
tf->update();
}
}

const auto buffer = tf->getBuffer();
std::string_view string{reinterpret_cast<const char *>(buffer.data()), buffer.size()};
return crow::response("image/png", std::string(string));
});

// Starts recording
CROW_ROUTE(app, "/start/<string>").methods("POST"_method)([&rec](const crow::request& req, std::string_view filename) {
if (rec.running())
return crow::response(crow::status::BAD_REQUEST);
const char *format = req.url_params.get("format");
rec.start(filename, format ? format : "mkv");
return crow::response(crow::status::NO_CONTENT);
});

// Stops recording
CROW_ROUTE(app, "/stop").methods("POST"_method)([&rec](const crow::request& req) {
if (!rec.running())
return crow::response(crow::status::BAD_REQUEST);
rec.stop();
return crow::response(crow::status::NO_CONTENT);
});

// Download recording
CROW_ROUTE(app, "/recording/<string>").methods("GET"_method)([&rec](const crow::request& req, std::string_view filename) {
const auto path = rec.file(filename);
if (!std::filesystem::exists(path))
return crow::response(crow::status::NOT_FOUND);

// Create a streaming response for better memory efficiency with large files
crow::response fres;

// Set content type based on file extension
std::string extension = path.extension().string();

std::string content_type;
if (extension == ".mkv")
content_type = "video/x-matroska";
else if (extension == ".mp4")
content_type = "video/mp4";
else if (extension == ".webm")
content_type = "video/webm";
else if (extension == ".avi")
content_type = "video/x-msvideo";
else if (extension == ".mov")
content_type = "video/quicktime";
else
content_type = "video/" + extension.substr(1);

fres.set_header("Content-Type", content_type);

// Stream the file content
fres.set_static_file_info(path.string());

return fres;
});

// Delete recording
CROW_ROUTE(app, "/recording/<string>").methods("DELETE"_method)([&rec](const crow::request& req, std::string_view filename) {
const auto path = rec.file(filename);
if (!std::filesystem::exists(path))
return crow::response(crow::status::NOT_FOUND);
std::filesystem::remove(path);
return crow::response(crow::status::NO_CONTENT);
});


Write a HTML/CSS/Javascript frontend for the backend above. Keep the UI simple yet effective, and mobile friendly.
Refresh the /thumbnail endpoint every second to get a live image of the camera. Note that this is only possible while the camera is recording.
Do not use any libraries.
The frontend will be located in /static/

Files changed (6) hide show
  1. README.md +8 -5
  2. components/footer.js +49 -0
  3. components/header.js +48 -0
  4. index.html +121 -19
  5. script.js +267 -0
  6. style.css +90 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Screenstream Studio
3
- emoji: 🌍
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: ScreenStream Studio 📹
3
+ colorFrom: red
4
+ colorTo: red
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/footer.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class CustomFooter extends HTMLElement {
2
+ connectedCallback() {
3
+ this.attachShadow({ mode: 'open' });
4
+ this.shadowRoot.innerHTML = `
5
+ <style>
6
+ .footer {
7
+ background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
8
+ }
9
+
10
+ .link {
11
+ transition: all 0.2s ease-in-out;
12
+ }
13
+
14
+ .link:hover {
15
+ transform: translateY(-1px);
16
+ }
17
+ </style>
18
+ <footer class="footer mt-12">
19
+ <div class="container mx-auto px-4 py-8">
20
+ <div class="flex flex-col md:flex-row items-center justify-between gap-4">
21
+ <div class="text-gray-400 text-sm">
22
+ © ${new Date().getFullYear()} ScreenStream Studio. All streams preserved.
23
+ </div>
24
+ <div class="flex items-center gap-6">
25
+ <a href="#" class="link text-gray-400 hover:text-white transition-colors">
26
+ <i data-feather="help-circle"></i>
27
+ </a>
28
+ <a href="#" class="link text-gray-400 hover:text-white transition-colors">
29
+ <i data-feather="github"></i>
30
+ </a>
31
+ <a href="#" class="link text-gray-400 hover:text-white transition-colors">
32
+ <i data-feather="settings"></i>
33
+ </a>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </footer>
38
+ `;
39
+
40
+ // Ensure feather icons are replaced in shadow DOM
41
+ setTimeout(() => {
42
+ if (this.shadowRoot.querySelector('[data-feather]')) {
43
+ feather.replace(this.shadowRoot);
44
+ }
45
+ }, 100);
46
+ }
47
+ }
48
+
49
+ customElements.define('custom-footer', CustomFooter);
components/header.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class CustomHeader extends HTMLElement {
2
+ connectedCallback() {
3
+ this.attachShadow({ mode: 'open' });
4
+ this.shadowRoot.innerHTML = `
5
+ <style>
6
+ .header {
7
+ background: linear-gradient(135deg, #3B82F6 0%, #1D4ED8 100%);
8
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
9
+ }
10
+
11
+ .logo {
12
+ background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
13
+ -webkit-background-clip: text;
14
+ -webkit-text-fill-color: transparent;
15
+ background-clip: text;
16
+ font-weight: 700;
17
+ }
18
+
19
+ @media (max-width: 768px) {
20
+ .logo {
21
+ font-size: 1.25rem;
22
+ }
23
+ }
24
+ </style>
25
+ <header class="header">
26
+ <div class="container mx-auto px-4 py-4">
27
+ <div class="flex items-center justify-between">
28
+ <div class="flex items-center gap-3">
29
+ <div class="p-2 bg-white/20 rounded-lg">
30
+ <i data-feather="video" class="text-white"></i>
31
+ </div>
32
+ <h1 class="logo text-2xl md:text-3xl">ScreenStream Studio 📹</h1>
33
+ <div class="w-8"></div> <!-- Spacer for balance -->
34
+ </div>
35
+ </div>
36
+ </header>
37
+ `;
38
+
39
+ // Ensure feather icons are replaced in shadow DOM
40
+ setTimeout(() => {
41
+ if (this.shadowRoot.querySelector('[data-feather]')) {
42
+ feather.replace(this.shadowRoot);
43
+ }
44
+ }, 100);
45
+ }
46
+ }
47
+
48
+ customElements.define('custom-header', CustomHeader);
index.html CHANGED
@@ -1,19 +1,121 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ScreenStream Studio 📹</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
10
+ <script src="https://unpkg.com/feather-icons"></script>
11
+ <link rel="stylesheet" href="style.css">
12
+ <script>
13
+ tailwind.config = {
14
+ theme: {
15
+ extend: {
16
+ colors: {
17
+ primary: '#3B82F6',
18
+ secondary: '#10B981'
19
+ }
20
+ }
21
+ }
22
+ }
23
+ </script>
24
+ </head>
25
+ <body class="bg-gray-50 min-h-screen">
26
+ <custom-header></custom-header>
27
+
28
+ <main class="container mx-auto px-4 py-8">
29
+ <!-- Live Preview Section -->
30
+ <section class="mb-8">
31
+ <div class="bg-white rounded-xl shadow-lg p-6">
32
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
33
+ <i data-feather="video"></i>
34
+ Live Preview
35
+ </h2>
36
+ <div id="livePreview" class="aspect-video bg-gray-900 rounded-lg overflow-hidden flex items-center justify-center">
37
+ <img id="thumbnail" src="" alt="Live Camera Feed" class="w-full h-full object-contain hidden">
38
+ <div id="noFeed" class="text-gray-400 text-center">
39
+ <i data-feather="camera-off" class="w-16 h-16 mx-auto mb-2"></i>
40
+ <p>Camera is not recording</p>
41
+ </div>
42
+ </div>
43
+ <div class="mt-4 flex items-center justify-between">
44
+ <div id="recordingStatus" class="flex items-center gap-2">
45
+ <div id="statusDot" class="w-3 h-3 bg-red-500 rounded-full animate-pulse hidden"></div>
46
+ <span id="statusText" class="text-gray-600">Not Recording</span>
47
+ </div>
48
+ <div id="duration" class="text-gray-600 font-mono"></div>
49
+ </div>
50
+ </div>
51
+ </section>
52
+
53
+ <!-- Controls Section -->
54
+ <section class="mb-8">
55
+ <div class="bg-white rounded-xl shadow-lg p-6">
56
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
57
+ <i data-feather="settings"></i>
58
+ Recording Controls
59
+ </h2>
60
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
61
+ <div>
62
+ <label for="filename" class="block text-sm font-medium text-gray-700 mb-2">
63
+ Filename
64
+ </label>
65
+ <input type="text" id="filename" placeholder="recording_001"
66
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
67
+ </div>
68
+ <div>
69
+ <label for="format" class="block text-sm font-medium text-gray-700 mb-2">
70
+ Format
71
+ </label>
72
+ <select id="format" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
73
+ <option value="mkv">MKV</option>
74
+ <option value="mp4">MP4</option>
75
+ <option value="webm">WebM</option>
76
+ <option value="avi">AVI</option>
77
+ <option value="mov">MOV</option>
78
+ </select>
79
+ </div>
80
+ </div>
81
+ <div class="flex gap-3 mt-6">
82
+ <button id="startBtn"
83
+ class="flex-1 bg-primary hover:bg-blue-600 text-white py-3 px-6 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2">
84
+ <i data-feather="play"></i>
85
+ Start Recording
86
+ </button>
87
+ <button id="stopBtn"
88
+ class="flex-1 bg-red-500 hover:bg-red-600 text-white py-3 px-6 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2"
89
+ disabled>
90
+ <i data-feather="square"></i>
91
+ Stop Recording
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </section>
96
+
97
+ <!-- Recordings Section -->
98
+ <section>
99
+ <div class="bg-white rounded-xl shadow-lg p-6">
100
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
101
+ <i data-feather="film"></i>
102
+ Recorded Videos
103
+ </h2>
104
+ <div id="recordingsList" class="space-y-3">
105
+ <!-- Recordings will be populated here -->
106
+ </div>
107
+ </div>
108
+ </section>
109
+ </main>
110
+
111
+ <custom-footer></custom-footer>
112
+
113
+ <script src="components/header.js"></script>
114
+ <script src="components/footer.js"></script>
115
+ <script src="script.js"></script>
116
+ <script>
117
+ feather.replace();
118
+ </script>
119
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
120
+ </body>
121
+ </html>
script.js ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ScreenStreamApp {
2
+ constructor() {
3
+ this.thumbnailInterval = null;
4
+ this.durationInterval = null;
5
+ this.isRecording = false;
6
+ this.init();
7
+ }
8
+
9
+ init() {
10
+ this.bindEvents();
11
+ this.loadRecordings();
12
+ this.checkRecordingStatus();
13
+ }
14
+
15
+ bindEvents() {
16
+ document.getElementById('startBtn').addEventListener('click', () => this.startRecording());
17
+ document.getElementById('stopBtn').addEventListener('click', () => this.stopRecording());
18
+ }
19
+
20
+ async checkRecordingStatus() {
21
+ try {
22
+ const response = await fetch('/duration');
23
+ if (response.ok) {
24
+ this.isRecording = true;
25
+ this.updateUIForRecording();
26
+ this.startLivePreview();
27
+ this.startDurationTimer();
28
+ } else {
29
+ this.isRecording = false;
30
+ this.updateUIForIdle();
31
+ this.stopLivePreview();
32
+ this.stopDurationTimer();
33
+ }
34
+ } catch (error) {
35
+ console.error('Error checking recording status:', error);
36
+ this.isRecording = false;
37
+ this.updateUIForIdle();
38
+ }
39
+ }
40
+
41
+ async startRecording() {
42
+ const filename = document.getElementById('filename').value.trim() || 'recording_' + Date.now();
43
+ const format = document.getElementById('format').value;
44
+
45
+ try {
46
+ const response = await fetch(`/start/${filename}?format=${format}`, {
47
+ method: 'POST'
48
+ });
49
+
50
+ if (response.status === 204) {
51
+ this.isRecording = true;
52
+ this.updateUIForRecording();
53
+ this.startLivePreview();
54
+ this.startDurationTimer();
55
+ this.showNotification('Recording started successfully!', 'success');
56
+ } else {
57
+ this.showNotification('Failed to start recording', 'error');
58
+ }
59
+ } catch (error) {
60
+ console.error('Error starting recording:', error);
61
+ this.showNotification('Error starting recording', 'error');
62
+ }
63
+ }
64
+
65
+ async stopRecording() {
66
+ try {
67
+ const response = await fetch('/stop', {
68
+ method: 'POST'
69
+ });
70
+
71
+ if (response.status === 204) {
72
+ this.isRecording = false;
73
+ this.updateUIForIdle();
74
+ this.stopLivePreview();
75
+ this.stopDurationTimer();
76
+ this.showNotification('Recording stopped successfully!', 'success');
77
+ this.loadRecordings();
78
+ } else {
79
+ this.showNotification('Failed to stop recording', 'error');
80
+ }
81
+ } catch (error) {
82
+ console.error('Error stopping recording:', error);
83
+ this.showNotification('Error stopping recording', 'error');
84
+ }
85
+ }
86
+
87
+ updateUIForRecording() {
88
+ document.getElementById('startBtn').disabled = true;
89
+ document.getElementById('stopBtn').disabled = false;
90
+ document.getElementById('statusDot').classList.remove('hidden');
91
+ document.getElementById('statusText').textContent = 'Recording';
92
+ document.getElementById('thumbnail').classList.remove('hidden');
93
+ document.getElementById('noFeed').classList.add('hidden');
94
+ }
95
+
96
+ updateUIForIdle() {
97
+ document.getElementById('startBtn').disabled = false;
98
+ document.getElementById('stopBtn').disabled = true;
99
+ document.getElementById('statusDot').classList.add('hidden');
100
+ document.getElementById('statusText').textContent = 'Not Recording';
101
+ document.getElementById('thumbnail').classList.add('hidden');
102
+ document.getElementById('noFeed').classList.remove('hidden');
103
+ document.getElementById('duration').textContent = '';
104
+ }
105
+
106
+ startLivePreview() {
107
+ this.stopLivePreview();
108
+ this.updateThumbnail();
109
+ this.thumbnailInterval = setInterval(() => this.updateThumbnail(), 1000);
110
+ }
111
+
112
+ stopLivePreview() {
113
+ if (this.thumbnailInterval) {
114
+ clearInterval(this.thumbnailInterval);
115
+ this.thumbnailInterval = null;
116
+ }
117
+ }
118
+
119
+ async updateThumbnail() {
120
+ const thumbnail = document.getElementById('thumbnail');
121
+ try {
122
+ const response = await fetch('/thumbnail');
123
+ if (response.ok) {
124
+ const blob = await response.blob();
125
+ const url = URL.createObjectURL(blob);
126
+ thumbnail.src = url + '?t=' + Date.now();
127
+ }
128
+ } catch (error) {
129
+ console.error('Error updating thumbnail:', error);
130
+ }
131
+ }
132
+
133
+ startDurationTimer() {
134
+ this.stopDurationTimer();
135
+ this.updateDuration();
136
+ this.durationInterval = setInterval(() => this.updateDuration(), 1000);
137
+ }
138
+
139
+ stopDurationTimer() {
140
+ if (this.durationInterval) {
141
+ clearInterval(this.durationInterval);
142
+ this.durationInterval = null;
143
+ }
144
+ }
145
+
146
+ async updateDuration() {
147
+ try {
148
+ const response = await fetch('/duration');
149
+ if (response.ok) {
150
+ const duration = await response.text();
151
+ document.getElementById('duration').textContent = this.formatDuration(parseInt(duration));
152
+ }
153
+ } catch (error) {
154
+ console.error('Error updating duration:', error);
155
+ }
156
+ }
157
+
158
+ formatDuration(seconds) {
159
+ const hours = Math.floor(seconds / 3600);
160
+ const minutes = Math.floor((seconds % 3600) / 60);
161
+ const secs = seconds % 60;
162
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
163
+ }
164
+
165
+ async loadRecordings() {
166
+ try {
167
+ const response = await fetch('/videos');
168
+ const videos = await response.json();
169
+ this.renderRecordings(videos);
170
+ } catch (error) {
171
+ console.error('Error loading recordings:', error);
172
+ }
173
+ }
174
+
175
+ renderRecordings(videos) {
176
+ const container = document.getElementById('recordingsList');
177
+
178
+ if (videos.length === 0) {
179
+ container.innerHTML = `
180
+ <div class="text-center py-8 text-gray-500">
181
+ <i data-feather="folder" class="w-12 h-12 mx-auto mb-4"></i>
182
+ <p>No recordings found</p>
183
+ </div>
184
+ `;
185
+ return;
186
+ }
187
+
188
+ container.innerHTML = videos.map(video => `
189
+ <div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:border-primary transition-colors">
190
+ <div class="flex items-center gap-3">
191
+ <i data-feather="video" class="text-primary"></i>
192
+ <div>
193
+ <h3 class="font-semibold text-gray-800">${video}</h3>
194
+ <p class="text-sm text-gray-500">Click to download</p>
195
+ </div>
196
+ </div>
197
+ <div class="flex gap-2">
198
+ <a href="/recording/${video}"
199
+ class="bg-primary hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1">
200
+ <i data-feather="download"></i>
201
+ Download
202
+ </a>
203
+ <button onclick="app.deleteRecording('${video}')"
204
+ class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1">
205
+ <i data-feather="trash-2"></i>
206
+ Delete
207
+ </button>
208
+ </div>
209
+ </div>
210
+ `).join('');
211
+
212
+ feather.replace();
213
+ }
214
+
215
+ async deleteRecording(filename) {
216
+ if (!confirm(`Are you sure you want to delete "${filename}"?`)) {
217
+ return;
218
+ }
219
+
220
+ try {
221
+ const response = await fetch(`/recording/${filename}`, {
222
+ method: 'DELETE'
223
+ });
224
+
225
+ if (response.status === 204) {
226
+ this.showNotification('Recording deleted successfully!', 'success');
227
+ this.loadRecordings();
228
+ } else {
229
+ this.showNotification('Failed to delete recording', 'error');
230
+ }
231
+ } catch (error) {
232
+ console.error('Error deleting recording:', error);
233
+ this.showNotification('Error deleting recording', 'error');
234
+ }
235
+ }
236
+
237
+ showNotification(message, type = 'info') {
238
+ // Create notification element
239
+ const notification = document.createElement('div');
240
+ notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transform transition-transform duration-300 translate-x-full ${
241
+ type === 'success' ? 'bg-secondary' :
242
+ type === 'error' ? 'bg-red-500' :
243
+ 'bg-primary'
244
+ } text-white font-medium`;
245
+ notification.textContent = message;
246
+
247
+ document.body.appendChild(notification);
248
+
249
+ // Animate in
250
+ setTimeout(() => {
251
+ notification.classList.remove('translate-x-full');
252
+ }, 100);
253
+
254
+ // Remove after delay
255
+ setTimeout(() => {
256
+ notification.classList.add('translate-x-full');
257
+ setTimeout(() => {
258
+ document.body.removeChild(notification);
259
+ }, 300);
260
+ }, 3000);
261
+ }
262
+ }
263
+
264
+ // Initialize the app when DOM is loaded
265
+ document.addEventListener('DOMContentLoaded', () => {
266
+ window.app = new ScreenStreamApp();
267
+ });
style.css CHANGED
@@ -1,28 +1,99 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
 
 
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for ScreenStream Studio */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
3
+
4
+ * {
5
+ font-family: 'Inter', sans-serif;
6
+ }
7
+
8
+ /* Custom scrollbar */
9
+ ::-webkit-scrollbar {
10
+ width: 6px;
11
+ }
12
+
13
+ ::-webkit-scrollbar-track {
14
+ background: #f1f5f9;
15
+ }
16
+
17
+ ::-webkit-scrollbar-thumb {
18
+ background: #cbd5e1;
19
+ border-radius: 3px;
20
+ }
21
+
22
+ ::-webkit-scrollbar-thumb:hover {
23
+ background: #94a3b8;
24
  }
25
 
26
+ /* Loading animation */
27
+ @keyframes pulse {
28
+ 0%, 100% {
29
+ opacity: 1;
30
+ }
31
+ 50% {
32
+ opacity: 0.5;
33
+ }
34
  }
35
 
36
+ .loading {
37
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 
 
 
38
  }
39
 
40
+ /* Recording status animation */
41
+ @keyframes recording-pulse {
42
+ 0%, 100% {
43
+ transform: scale(1);
44
+ opacity: 1;
45
+ }
46
+ 50% {
47
+ transform: scale(1.1);
48
+ opacity: 0.7;
49
+ }
50
  }
51
 
52
+ .recording-active {
53
+ animation: recording-pulse 1.5s ease-in-out infinite;
54
  }
55
+
56
+ /* Smooth transitions for all interactive elements */
57
+ button, a, input, select {
58
+ transition: all 0.2s ease-in-out;
59
+ }
60
+
61
+ /* Custom focus styles */
62
+ .focus-ring:focus {
63
+ outline: 2px solid #3B82F6;
64
+ outline-offset: 2px;
65
+ }
66
+
67
+ /* Responsive video grid */
68
+ .video-grid {
69
+ display: grid;
70
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
71
+ gap: 1rem;
72
+ }
73
+
74
+ /* Mobile optimizations */
75
+ @media (max-width: 768px) {
76
+ .container {
77
+ padding-left: 1rem;
78
+ padding-right: 1rem;
79
+ }
80
+
81
+ .video-grid {
82
+ grid-template-columns: 1fr;
83
+ }
84
+ }
85
+
86
+ /* Dark mode support */
87
+ @media (prefers-color-scheme: dark) {
88
+ .dark\:bg-gray-900 {
89
+ background-color: #111827;
90
+ }
91
+
92
+ .dark\:text-white {
93
+ color: #ffffff;
94
+ }
95
+
96
+ .dark\:bg-gray-800 {
97
+ background-color: #1f2937;
98
+ }
99
+ }