rustream/templates/
upload.rs

1/// Get the HTML content to render the upload page.
2///
3/// # See Also
4///
5/// - This page is served as a response for the `/upload` 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<html lang="en">
13<head>
14    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
15    <title>RuStream - Self-hosted Streaming Engine - v{{ version }}</title>
16    <meta property="og:type" content="MediaStreaming">
17    <meta name="keywords" content="Rust, streaming, actix, JavaScript, HTML, CSS">
18    <meta name="author" content="Vignesh Rao">
19    <meta content="width=device-width, initial-scale=1" name="viewport">
20    <!-- Favicon.ico and Apple Touch Icon -->
21    <link rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.ico">
22    <link rel="apple-touch-icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.png">
23    <!-- Font Awesome icons -->
24    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/fontawesome.min.css">
25    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/solid.css">
26    <style>
27        /* Google fonts with a backup alternative */
28        @import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap');
29        * {
30            font-family: 'Ubuntu', 'PT Serif', sans-serif;
31        }
32        body {
33            background-color: #6c7dac;
34            padding: 30px;
35            margin: 0;
36        }
37        .container {
38            text-align: center;
39            width: 100%;
40            max-width: 500px;
41            min-height: 435px;
42            margin: auto;
43            background-color: white;
44            border-radius: 16px;
45            box-shadow: rgba(255, 255, 255, 0.1) 0 1px 1px 0 inset, rgba(50, 50, 93, 0.25) 0 50px 100px -20px, rgba(0, 0, 0, 0.3) 0 30px 60px -30px;
46        }
47        .header-section {
48            padding: 25px 0;
49        }
50        .header-section h1 {
51            font-weight: 500;
52            font-size: 1.7rem;
53            text-transform: uppercase;
54            color: #707EA0;
55            margin: 0;
56            margin-bottom: 8px;
57        }
58        .header-section p,
59        .header-section label {
60            margin: 5px;
61            font-size: 0.95rem;
62            color: #707EA0;
63        }
64        .drop-section {
65            min-height: 250px;
66            border: 1px dashed #A8B3E3;
67            background-image: linear-gradient(180deg, white, #F1F6FF);
68            margin: 5px 35px 35px 35px;
69            border-radius: 12px;
70            position: relative;
71        }
72        .drop-section div.col:first-child {
73            opacity: 1;
74            visibility: visible;
75            transition-duration: 0.2s;
76            transform: scale(1);
77            width: 200px;
78            margin: auto;
79        }
80        .drop-section div.col:last-child {
81            font-size: 40px;
82            font-weight: 700;
83            color: #c0cae1;
84            position: absolute;
85            top: 0;
86            bottom: 0;
87            left: 0;
88            right: 0;
89            margin: auto;
90            width: 200px;
91            height: 55px;
92            pointer-events: none;
93            opacity: 0;
94            visibility: hidden;
95            transform: scale(0.6);
96            transition-duration: 0.2s;
97        }
98        /* use "drag-over-effect" class in js */
99        .drag-over-effect div.col:first-child {
100            opacity: 0;
101            visibility: hidden;
102            pointer-events: none;
103            transform: scale(1.1);
104        }
105        .drag-over-effect div.col:last-child {
106            opacity: 1;
107            visibility: visible;
108            transform: scale(1);
109        }
110        .drop-section .cloud-icon {
111            margin-top: 25px;
112            margin-bottom: 20px;
113        }
114        .drop-section span,
115        .drop-section button {
116            display: block;
117            margin: auto;
118            color: #707EA0;
119            margin-bottom: 10px;
120        }
121        .drop-section button {
122            color: white;
123            background-color: #5874C6;
124            border: none;
125            outline: none;
126            padding: 7px 20px;
127            border-radius: 8px;
128            margin-top: 20px;
129            cursor: pointer;
130            box-shadow: rgba(50, 50, 93, 0.25) 0 13px 27px -5px, rgba(0, 0, 0, 0.3) 0 8px 16px -8px;
131        }
132        .drop-section input {
133            display: none;
134        }
135        .list-section {
136            display: none;
137            text-align: left;
138            margin: 0 35px;
139            padding-bottom: 20px;
140        }
141        .list-section .list-title {
142            font-size: 0.95rem;
143            color: #707EA0;
144        }
145        .list-section li {
146            display: flex;
147            margin: 15px 0;
148            padding-top: 4px;
149            padding-bottom: 2px;
150            border-radius: 8px;
151            transition-duration: 0.2s;
152        }
153        .list-section li:hover {
154            box-shadow: #E3EAF9 0 0 4px 0, #E3EAF9 0 12px 16px 0;
155        }
156        .list-section li .col {
157            flex: .1;
158        }
159        .list-section li .col:nth-child(1) {
160            flex: .15;
161            text-align: center;
162        }
163        .list-section li .col:nth-child(2) {
164            flex: .75;
165            text-align: left;
166            font-size: 0.9rem;
167            color: #3e4046;
168            padding: 8px 10px;
169        }
170        .list-section li .col:nth-child(2) div.name {
171            overflow: hidden;
172            white-space: nowrap;
173            text-overflow: ellipsis;
174            max-width: 250px;
175            display: inline-block;
176        }
177        .list-section li .col .file-name span {
178            color: #707EA0;
179            float: right;
180        }
181        .list-section li .file-progress {
182            width: 100%;
183            height: 5px;
184            margin-top: 8px;
185            border-radius: 8px;
186            background-color: #dee6fd;
187        }
188        .list-section li .file-progress span {
189            display: block;
190            width: 0%;
191            height: 100%;
192            border-radius: 8px;
193            background-image: linear-gradient(120deg, #6b99fd, #9385ff);
194            transition-duration: 0.4s;
195        }
196        .list-section li .col .file-size {
197            font-size: 0.75rem;
198            margin-top: 3px;
199            color: #707EA0;
200        }
201        .list-section li .col svg.cross,
202        .list-section li .col svg.tick {
203            fill: #8694d2;
204            background-color: #dee6fd;
205            position: relative;
206            left: 50%;
207            top: 50%;
208            transform: translate(-50%, -50%);
209            border-radius: 50%;
210        }
211        .list-section li .col svg.tick {
212            fill: #50a156;
213            background-color: transparent;
214        }
215        .list-section li.complete span,
216        .list-section li.complete .file-progress,
217        .list-section li.complete svg.cross {
218            display: none;
219        }
220        .list-section li.in-prog .file-size,
221        .list-section li.in-prog svg.tick {
222            display: none;
223        }
224    </style>
225    <style>
226        a,
227        button {
228            color: white;
229            background-color: #6c7dac;
230        }
231        .upload {
232            position: absolute;
233            top: 3.8%;
234            right: 313px;
235            border: none;
236            padding: 10px 14px;
237            font-size: 16px;
238            cursor: pointer;
239        }
240        .home {
241            position: absolute;
242            top: 3.8%;
243            right: 217px;
244            border: none;
245            padding: 10px 14px;
246            font-size: 16px;
247            cursor: pointer;
248        }
249        .back {
250            position: absolute;
251            top: 3.8%;
252            right: 132px;
253            border: none;
254            padding: 10px 14px;
255            font-size: 16px;
256            cursor: pointer;
257        }
258    </style>
259    <style>
260        .dropbtn {
261            position: absolute;
262            top: 3.8%;
263            right: 30px;
264            padding: 10px 24px;
265            font-size: 16px;
266            border: none;
267            cursor: pointer;
268        }
269        .dropdown {
270            position: absolute;
271            top: 3.8%;
272            right: 30px;
273            padding: 10px 24px;
274            display: inline-block;
275        }
276        .dropdown-content {
277            display: none;
278            position: absolute;
279            top: 40px;  /* Distance from the user icon button */
280            right: 30px;
281            width: 160px;
282            min-width: auto;
283            box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);  /* Basically, black with 20% opacity */
284            z-index: 1;
285        }
286        .dropdown-content a {
287            padding: 12px 16px;
288            text-decoration: none;
289            display: block;
290        }
291        .dropdown:hover .dropdown-content {display: block;}
292    </style>
293</head>
294<noscript>
295    <style>
296        body {
297            width: 100%;
298            height: 100%;
299            overflow: hidden;
300        }
301    </style>
302    <div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515;">
303        <h2 style="margin-top:5%">This page requires JavaScript
304            to be enabled.
305            <br><br>
306            Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
307        </h2>
308        <form>
309            <button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
310        </form>
311    </div>
312</noscript>
313<body>
314    <button class="upload" onclick="upload()"><i class="fa-solid fa-cloud-arrow-up"></i> Upload</button>
315    <button class="home" onclick="goHome()"><i class="fa fa-home"></i> Home</button>
316    <button class="back" onclick="goBack()"><i class="fa fa-backward"></i> Back</button>
317    <div class="dropdown">
318        <button class="dropbtn"><i class="fa fa-user"></i></button>
319        <div class="dropdown-content">
320            <a onclick="goProfile()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
321            <a onclick="logOut()" style="cursor: pointer"><i class="fa fa-sign-out"></i> logout</a>
322        </div>
323    </div>
324    <br><br><br>
325    <div class="container">
326        <div class="header-section">
327            <h1>Upload Files</h1>
328            <p>PDF, Images, Videos and Subtitles are allowed</p>
329            <br>
330            <input type="checkbox" id="dedicated" name="dedicated" title="Files will be stored in a secured location, which can only be accessed by '{{ user }}'" checked>
331            <label for="dedicated" title="Files will be stored in a secured location, which can only be accessed by '{{ user }}'"><i class="fa-solid fa-lock"></i></i>&nbsp;&nbsp;Upload files to '{{ user }}' directory</label>
332        </div>
333        <div class="drop-section">
334            <div class="col">
335                <div class="cloud-icon">
336                    <img src="https://thevickypedia.github.io/open-source/images/icons/cloud.png" alt="cloud">
337                </div>
338                <span>Drag & Drop your files here</span>
339                <span>OR</span>
340                <button class="file-selector">Browse Files</button>
341                <input type="file" class="file-selector-input" multiple>
342            </div>
343            <div class="col">
344                <div class="drop-here">Drop Here</div>
345            </div>
346        </div>
347        <div class="list-section">
348            <div class="list-title">Uploaded Files</div>
349            <div class="list"></div>
350        </div>
351    </div>
352    <script>
353        const dropArea = document.querySelector('.drop-section')
354        const listSection = document.querySelector('.list-section')
355        const listContainer = document.querySelector('.list')
356        const fileSelector = document.querySelector('.file-selector')
357        const fileSelectorInput = document.querySelector('.file-selector-input')
358
359        // Upload files with browse button
360        fileSelector.onclick = () => fileSelectorInput.click()
361        fileSelectorInput.onchange = () => {
362            [...fileSelectorInput.files].forEach((file) => {
363                if (typeValidation(file.type)) {
364                    uploadFile(file)
365                }
366            })
367        }
368
369        // Check the file type
370        function typeValidation(type) {
371            let splitType = type.split('/')[0]
372            if (type === 'application/pdf' || type === 'text/vtt' || splitType === 'image' || splitType === 'video') {
373                return true
374            }
375        }
376
377        // When file is over the drag area
378        dropArea.ondragover = (e) => {
379            e.preventDefault();
380            [...e.dataTransfer.items].forEach((item) => {
381                if (typeValidation(item.type)) {
382                    dropArea.classList.add('drag-over-effect')
383                }
384            })
385        }
386        // When file leave the drag area
387        dropArea.ondragleave = () => {
388            dropArea.classList.remove('drag-over-effect')
389        }
390        // When file drop on the drag area
391        dropArea.ondrop = (e) => {
392            e.preventDefault();
393            dropArea.classList.remove('drag-over-effect')
394            if (e.dataTransfer.items) {
395                [...e.dataTransfer.items].forEach((item) => {
396                    if (item.kind === 'file') {
397                        const file = item.getAsFile();
398                        if (typeValidation(file.type)) {
399                            uploadFile(file)
400                        }
401                    }
402                })
403            } else {
404                [...e.dataTransfer.files].forEach((file) => {
405                    if (typeValidation(file.type)) {
406                        uploadFile(file)
407                    }
408                })
409            }
410        }
411        // upload file function
412        function uploadFile(file) {
413            listSection.style.display = 'block'
414            let li = document.createElement('li')
415            li.classList.add('in-prog')
416            li.innerHTML = `
417                <div class="col">
418                    <img src="https://thevickypedia.github.io/open-source/images/icons/${iconSelector(file.type)}" alt="">
419                </div>
420                <div class="col">
421                    <div class="file-name">
422                        <div class="name">${file.name}</div>
423                        <span>0%</span>
424                    </div>
425                    <div class="file-progress">
426                        <span></span>
427                    </div>
428                    <div class="file-size">${(file.size / (1024 * 1024)).toFixed(2)} MB</div>
429                </div>
430                <div class="col">
431                    <svg xmlns="http://www.w3.org/2000/svg" class="cross" height="20" width="20"><path d="m5.979 14.917-.854-.896 4-4.021-4-4.062.854-.896 4.042 4.062 4-4.062.854.896-4 4.062 4 4.021-.854.896-4-4.063Z"/></svg>
432                    <svg xmlns="http://www.w3.org/2000/svg" class="tick" height="20" width="20"><path d="m8.229 14.438-3.896-3.917 1.438-1.438 2.458 2.459 6-6L15.667 7Z"/></svg>
433                </div>
434            `
435            listContainer.prepend(li)
436            let http = new XMLHttpRequest()
437            let checkbox = document.getElementById('dedicated');
438            let data = new FormData()
439            data.append('file', file)
440            http.onload = () => {
441                checkbox.disabled = false;
442                if (http.status === 200) {
443                    // Successful response from the server
444                    li.classList.add('complete');
445                    li.classList.remove('in-prog');
446                } else {
447                    // Handle error responses
448                    alert('Error uploading file. Status:' + http.status);
449                    return false;
450                }
451            }
452            http.onerror = (error) => {
453                // Handle network errors
454                console.log(error);
455                alert('Network error during file upload.');
456                checkbox.disabled = false;
457                return false;
458            };
459            http.upload.onprogress = (e) => {
460                checkbox.disabled = true;
461                let percent_complete = (e.loaded / e.total) * 100
462                li.querySelectorAll('span')[0].innerHTML = Math.round(percent_complete) + '%'
463                li.querySelectorAll('span')[1].style.width = percent_complete + '%'
464            }
465            http.open('POST', window.location.origin + '/upload', true);  // asynchronous session
466            http.setRequestHeader('secure-flag', checkbox.checked);
467            http.send(data)
468            li.querySelector('.cross').onclick = () => http.abort()
469            http.onabort = () => {
470                checkbox.disabled = false;
471                let crossElement = li.querySelector('.cross');
472                // Insert a red cross sign
473                crossElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="cross-solid" height="20" width="20"><path d="m5.979 14.917-.854-.896 4-4.021-4-4.062.854-.896 4.042 4.062 4-4.062.854.896-4 4.062 4 4.021-.854.896-4-4.063Z" stroke="red" stroke-width="2"></path></svg>';
474                let context = li.querySelector('.file-name');
475                let spanElement = context.querySelector('span');
476                // Change uploaded percentage into a text
477                spanElement.innerHTML = 'ABORTED!!';
478            }
479        }
480
481        // find icon for file
482        function iconSelector(type) {
483            let splitType = (type.split('/')[0] === 'application') ? type.split('/')[1] : type.split('/')[0];
484            return splitType + '.png'
485        }
486    </script>
487    <script>
488        function goHome() {
489            window.location.href = "/home";
490        }
491        function goProfile() {
492            window.location.href = '/profile';
493        }
494        function logOut() {
495            window.location.href = "/logout";
496        }
497        function upload() {
498            window.location.href = "/upload";
499        }
500        function goBack() {
501            window.history.back();
502        }
503    </script>
504    <script>
505        document.getElementById("dedicated").addEventListener("change", function() {
506            if (!this.checked) {
507                let confirmation = confirm(
508                    "Uploading to public space will not only make your uploads accessible " +
509                    "to other users, but it will also overwrite any existing files " +
510                    "with the same name.\n\nAre you sure you want to proceed?"
511                );
512                if (!confirmation) {
513                    this.checked = true;
514                }
515            }
516        });
517    </script>
518</body>
519</html>"#.to_string()
520}