rustream/templates/
listing.rs

1/// Get the HTML content to render the home/listing page.
2///
3/// # See Also
4///
5/// - This page is served as a response for the `/home` entry point.
6///
7/// # Returns
8///
9/// A `String` version of the HTML, CSS and JS content.
10pub 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>&nbsp;&nbsp;Delete</div>
196        <div class="context-menu-item" onclick="editItem(currentPath, 'rename')"><i class="fa-solid fa-pen"></i></i>&nbsp;&nbsp;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>&nbsp;&nbsp;<a oncontextmenu="showContextMenu(event, '{{ file.path }}')" href="{{ file.path }}">{{ file.name }}</a></li>
215                {% else %}
216                    <li><i class="{{ file.font }}"></i>&nbsp;&nbsp;<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>&nbsp;&nbsp;<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>&nbsp;&nbsp;<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}