rustream/templates/
listing.rs1pub fn get_content() -> String {
11 r###"<!DOCTYPE html>
12<!--suppress JSUnresolvedLibraryURL -->
13<html lang="en">
14<head>
15 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
16 <title>RuStream - Self-hosted Streaming Engine - v{{ version }}</title>
17 <meta property="og:type" content="MediaStreaming">
18 <meta name="keywords" content="Rust, streaming, actix, JavaScript, HTML, CSS">
19 <meta name="author" content="Vignesh Rao">
20 <meta content="width=device-width, initial-scale=1" name="viewport">
21 <!-- Favicon.ico and Apple Touch Icon -->
22 <link rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.ico">
23 <link rel="apple-touch-icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.png">
24 <!-- Font Awesome icons -->
25 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/fontawesome.min.css">
26 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/solid.css">
27 <!-- CSS and JS for night mode -->
28 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
29 <script type="text/javascript" src="https://thevickypedia.github.io/open-source/nightmode/night.js" defer></script>
30 <link rel="stylesheet" type="text/css" href="https://thevickypedia.github.io/open-source/nightmode/night.css">
31 <!-- Button CSS -->
32 <style>
33 /* Google fonts with a backup alternative */
34 @import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap');
35 * {
36 font-family: 'Ubuntu', 'PT Serif', sans-serif;
37 }
38 body {
39 margin-left: 1%; /* 1% away from left corner */
40 padding: 0.5% /* 0.5% away from any surrounding elements */
41 }
42 small {
43 font-size: 16px;
44 }
45 .upload {
46 position: absolute;
47 top: 3.8%;
48 right: 313px;
49 border: none;
50 padding: 10px 14px;
51 font-size: 16px;
52 cursor: pointer;
53 }
54 .home {
55 position: absolute;
56 top: 3.8%;
57 right: 217px;
58 border: none;
59 padding: 10px 14px;
60 font-size: 16px;
61 cursor: pointer;
62 }
63 .back {
64 position: absolute;
65 top: 3.8%;
66 right: 132px;
67 border: none;
68 padding: 10px 14px;
69 font-size: 16px;
70 cursor: pointer;
71 }
72 </style>
73 <style>
74 .dropbtn {
75 position: absolute;
76 top: 3.8%;
77 right: 30px;
78 padding: 10px 24px;
79 font-size: 16px;
80 border: none;
81 cursor: pointer;
82 }
83 .dropdown {
84 position: absolute;
85 top: 3.8%;
86 right: 30px;
87 padding: 10px 24px;
88 display: inline-block;
89 }
90 .dropdown-content {
91 display: none;
92 position: absolute;
93 top: 40px; /* Distance from the user icon button */
94 right: 30px;
95 width: 160px;
96 min-width: auto;
97 box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); /* Basically, black with 20% opacity */
98 z-index: 1;
99 }
100 .dropdown-content a {
101 padding: 12px 16px;
102 text-decoration: none;
103 display: block;
104 }
105 .dropdown:hover .dropdown-content {display: block;}
106 </style>
107 <!-- Title list CSS -->
108 <style>
109 a:hover, a:active { font-size: 102%; opacity: 0.5; }
110 a:link { color: blue; }
111 a:visited { color: blue; }
112 ol {
113 list-style: none;
114 counter-reset: list-counter;
115 }
116 li {
117 margin: 1rem;
118 list-style-type: none; /* Hide default marker */
119 }
120 li::before {
121 background: #4169E1;
122 width: 2rem;
123 height: 2rem;
124 border-radius: 50%;
125 display: inline-block;
126 line-height: 2rem;
127 color: white;
128 text-align: center;
129 margin-right: 0.5rem;
130 }
131 </style>
132 <style>
133 /* Style for context menu */
134 .context-menu {
135 position: absolute;
136 background-color: #fff;
137 border: 1px solid #ccc;
138 padding: 5px 0;
139 box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
140 }
141 .context-menu-item {
142 padding: 5px 10px;
143 cursor: pointer;
144 background-color: #fff !important; /* White background */
145 color: #000 !important; /* Black font */
146 }
147 .context-menu-item:hover {
148 background-color: #000 !important; /* Black background */
149 color: #fff !important; /* White font */
150 }
151 .icon {
152 background-color: #fff !important; /* White background */
153 color: #000 !important; /* Black font */
154 }
155 .icon:hover {
156 background-color: #000 !important; /* Black background */
157 color: #fff !important; /* White font */
158 }
159 </style>
160</head>
161<noscript>
162 <style>
163 body {
164 width: 100%;
165 height: 100%;
166 overflow: hidden;
167 }
168 </style>
169 <div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515;">
170 <h2 style="margin-top:5%">This page requires JavaScript
171 to be enabled.
172 <br><br>
173 Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
174 </h2>
175 <form>
176 <button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
177 </form>
178 </div>
179</noscript>
180<body translate="no">
181 <div class="toggler fa fa-moon-o"></div>
182 <button class="upload" onclick="upload()"><i class="fa-solid fa-cloud-arrow-up"></i> Upload</button>
183 <button class="home" onclick="goHome()"><i class="fa fa-home"></i> Home</button>
184 <button class="back" onclick="goBack()"><i class="fa fa-backward"></i> Back</button>
185 <div class="dropdown">
186 <button class="dropbtn"><i class="fa fa-user"></i></button>
187 <div class="dropdown-content">
188 <a onclick="goProfile()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
189 <a onclick="logOut()" style="cursor: pointer"><i class="fa fa-sign-out"></i> logout</a>
190 </div>
191 </div>
192 <br><br><br><br>
193 <!-- Context menu template (hidden by default) -->
194 <div id="contextMenu" class="context-menu icon" style="display: none;">
195 <div class="context-menu-item" onclick="editItem(currentPath, 'delete')"><i class="fa-regular fa-trash-can"></i> Delete</div>
196 <div class="context-menu-item" onclick="editItem(currentPath, 'rename')"><i class="fa-solid fa-pen"></i></i> Rename</div>
197 </div>
198 {% if custom_title %}
199 <h1>{{ custom_title }}</h1>
200 {% else %}
201 <h1>Welcome to RuStream <small>v{{ version }}</small></h1>
202 {% endif %}
203 <hr>
204 {% if dir_name or files or directories or secured_directories %}
205 <!-- Display directory name if within subdir -->
206 {% if dir_name %}
207 <h3>{{ dir_name }}</h3>
208 {% endif %}
209 <!-- Display number of files and list the files -->
210 {% if files %}
211 <h3>Files {{ files|length }}</h3>
212 {% for file in files %}
213 {% if secure_path == 'true' %}
214 <li><i class="{{ file.font }}"></i> <a oncontextmenu="showContextMenu(event, '{{ file.path }}')" href="{{ file.path }}">{{ file.name }}</a></li>
215 {% else %}
216 <li><i class="{{ file.font }}"></i> <a href="{{ file.path }}">{{ file.name }}</a></li>
217 {% endif %}
218 {% endfor %}
219 {% endif %}
220 <!-- Display number of directories and list the directories -->
221 {% if directories %}
222 <h3>Directories {{ directories|length }}</h3>
223 {% for directory in directories %}
224 <li><i class="{{ directory.font }}"></i> <a href="{{ directory.path }}">{{ directory.name }}</a></li>
225 {% endfor %}
226 {% endif %}
227 {% if secured_directories %}
228 <h3>Secured Directory</h3>
229 {% for directory in secured_directories %}
230 <li><i class="{{ directory.font }}"></i> <a oncontextmenu="showContextMenu(event, '{{ directory.path }}', true)" href="{{ directory.path }}">{{ directory.name }}</a></li>
231 {% endfor %}
232 {% endif %}
233 {% else %}
234 <h3 style="text-align: center">No content was rendered by the server</h3>
235 {% endif %}
236 <hr>
237 <script>
238 function goHome() {
239 window.location.href = "/home";
240 }
241 function goProfile() {
242 window.location.href = '/profile';
243 }
244 function logOut() {
245 window.location.href = "/logout";
246 }
247 function upload() {
248 window.location.href = "/upload";
249 }
250 function goBack() {
251 window.history.back();
252 }
253 </script>
254 <script>
255 var contextMenu = document.getElementById('contextMenu');
256
257 // Function to show context menu
258 function showContextMenu(event, path, isDir = false) {
259 event.preventDefault();
260
261 // Set the global variable to the current file path
262 currentPath = path;
263 directory = isDir;
264
265 // Calculate the appropriate coordinates for the context menu
266 var mouseX = event.clientX;
267 var mouseY = event.clientY;
268 var windowWidth = window.innerWidth;
269 var windowHeight = window.innerHeight;
270 var contextMenuWidth = contextMenu.offsetWidth;
271 var contextMenuHeight = contextMenu.offsetHeight;
272 var scrollX = window.scrollX || window.pageXOffset;
273 var scrollY = window.scrollY || window.pageYOffset;
274
275 // Adjust the coordinates considering the scroll position and moving 2 pixels away from the mouse pointer
276 var menuX = mouseX + scrollX + contextMenuWidth > windowWidth ? mouseX + scrollX - contextMenuWidth - 2 : mouseX + scrollX + 2;
277 var menuY = mouseY + scrollY + contextMenuHeight > windowHeight ? mouseY + scrollY - contextMenuHeight - 2 : mouseY + scrollY + 2;
278
279 // Position the context menu at the calculated coordinates
280 contextMenu.style.left = menuX + 'px';
281 contextMenu.style.top = menuY + 'px';
282 contextMenu.style.display = 'block';
283 }
284
285 function editAction(action, trueURL, relativePath, newName) {
286 let http = new XMLHttpRequest();
287 http.open('POST', window.location.origin + `/edit`, true); // asynchronous session
288 http.setRequestHeader('Content-Type', 'application/json'); // Set content type to JSON
289 http.setRequestHeader('edit-action', action);
290 http.onreadystatechange = function() {
291 if (http.readyState === XMLHttpRequest.DONE) {
292 if (http.status === 200) {
293 window.location.reload();
294 } else {
295 if (http.responseText !== "") {
296 alert(`Error: ${http.responseText}`);
297 } else {
298 alert(`Error: ${http.statusText}`);
299 }
300 }
301 }
302 };
303 let data = {
304 url_locator: trueURL,
305 path_locator: relativePath,
306 new_name: newName
307 };
308 http.send(JSON.stringify(data));
309 }
310
311 function getConfirmation(fileName, action) {
312 let confirmation = confirm(`Are you sure you want to ${action}?\n\n'${fileName}'`);
313 if (!confirmation) {
314 contextMenu.style.display = 'none';
315 return false;
316 }
317 return true;
318 }
319
320 function extractFileName(path) {
321 // Find the last occurrence of either '/' or '\'
322 const lastIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
323
324 // Extract the filename using substring
325 return path.substring(lastIndex + 1);
326 }
327
328 function isValidName(oldName, newName) {
329 // Condition 1 - Validate if the new filename is the same as old.
330 if (oldName === newName) {
331 alert(`New name is the same as old\n\n'${oldName}'=='${newName}'`);
332 }
333 // Condition 2 - Validate if the new filename starts or ends with . or _
334 if (newName.startsWith('_') || newName.endsWith('_') ||
335 newName.startsWith('.') || newName.endsWith('.')) {
336 alert(`New name cannot start or end with '.' or '_'\n\n${newName}`);
337 return false;
338 }
339 // Condition 3 - Validate if the new filename and the old has the same file extension.
340 const oldExtension = oldName.split('.').pop();
341 const newExtension = newName.split('.').pop();
342 // Check condition 3
343 if (oldExtension !== newExtension) {
344 alert(`File extension cannot be changed\n\n'${newExtension}' => '${oldExtension}'`);
345 return false;
346 }
347 // Condition 4 - Validate if the new filename has at least one character, apart from the file extension.
348 if (newName.length <= oldExtension.length + 1) {
349 alert(`At least one character is required as filename\n\nReceived ${newName.length}`);
350 return false;
351 }
352 return true;
353 }
354
355 // Function to handle delete/rename action
356 function editItem(relativePath, action) {
357 contextMenu.style.display = 'none';
358
359 let fileName = extractFileName(relativePath);
360 if (action === 'delete') {
361 let pass = getConfirmation(fileName, action);
362 if (!pass) {
363 return;
364 }
365 var newName = null;
366 } else {
367 if (directory) {
368 alert("Only a 'delete' action is permitted on directories");
369 return;
370 }
371 var newName = prompt(`Enter a new name for the file\n\nCurrent: ${fileName}\n`);
372 if (!isValidName(fileName, newName)) {
373 return;
374 }
375 }
376 let trueURL = window.location.href + '/' + fileName;
377 editAction(action, trueURL, relativePath, newName);
378 }
379
380 // Hide context menu when clicking outside
381 document.addEventListener('click', function(event) {
382 if (event.target !== contextMenu && !contextMenu.contains(event.target)) {
383 contextMenu.style.display = 'none';
384 }
385 });
386 </script>
387</body>
388</html>
389"###.to_string()
390}