Преглед на файлове

Scaled images and srcsets for better performance. Some paths and URLs inconsistencies fixed. Videos can now be played. Some CSS improvements.

Iñigo Valentin преди 10 месеца
родител
ревизия
6c5d305634

+ 3 - 1
backend/.gitignore

@@ -1,11 +1,13 @@
 # Database
-data.sqlite3
+data.sqlite3*
+data.sql
 
 # Environment files
 environment/env.*
 
 # Assets
 assets/images/projects/
+assets/images_scaled/
 
 # Node.js
 node_modules/

+ 24 - 0
backend/assets/generate_srcsets.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+dimensions=( 100 200 300 400 500 600 700 800 900 1000 );
+cd $(dirname "$0")
+pwd;
+rm -rf images_scaled;
+mkdir -p images_scaled;
+
+for dimension in "${dimensions[@]}";
+do
+	echo Generating x$dimension...;
+	mkdir -p images_scaled/x${dimension};
+	cp -rf images/* images_scaled/x$dimension/;
+	cd images_scaled/x$dimension/;
+	for file in $(find  -type f);
+	do
+		type=$(file -b --mime-type $file);
+		if [ "image" == "${type:0:5}" ];
+		then
+			#echo "IMAGE: ${file}";
+			convert $file -resize $dimension"x"${dimension}\> ${file}
+		fi
+	done;
+	cd ../../
+done;

+ 1 - 1
backend/src/data/data.js

@@ -6,7 +6,7 @@ let data = new sqlite3.Database(process.env.DB , (err) => {
 });
 
 data.langQuery = function(param, lang = null){
-    if (lang == null) lang = 'en';
+    if (lang == null) lang = 'es';
     if (param != null)
         return(
           "(SELECT text FROM text WHERE id = " + param + " AND (lang IS NULL OR lang = '"

+ 10 - 0
backend/src/routers/assetRouter.js

@@ -19,6 +19,16 @@ var mime = {
 router.get("/images/projects/:projectId/:imagePath", async (req, res) => {
     var reqpath = req.url.toString().split('?')[0];
     var file = "./assets" + reqpath.replace(/\/$/, '');
+    var scale = parseInt(req.query.w);
+    // Serve a scaled image if width has been specified.
+    if (parseInt(req.query.w) >= 0){
+        var width = 100;
+        for (var i = 100; i < 1000; i += 100){
+            width = i;
+            if (width >= scale) break;
+        }
+        file = file.replace("/assets/images/", "/assets/images_scaled/x" + width + "/");
+    }
     var type = mime[path.extname(file).slice(1)] || 'text/plain';
     var s = fs.createReadStream(file);
     s.on('open', function () {

+ 1 - 0
frontend/src/app/home/home.html

@@ -6,6 +6,7 @@
         <img
           class="project-image" id="img-project-{{project.id}}"
           src="{{ apiURL }}{{ project.images[0].path }}"
+          srcset="{{ utilService.generateSrcset(apiURL + project.images[0].path) }}"
         />
         <div class='project_details'>
           <h3>{{ project.title }}</h3>

+ 5 - 24
frontend/src/app/home/home.scss

@@ -17,7 +17,7 @@ section#catalog{
         margin: 0.5em;
         font-size: initial;
         
-        img.project-image{
+        img.project-image, img.project-image-alt{
             object-fit: cover;
             object-position: center;
             width: 100%;
@@ -28,20 +28,7 @@ section#catalog{
             left: 0;
             transition: opacity 0.5s ease-in-out, scale 0.5s ease-in-out;
         }
-        
-        img.project-image-alt{
-            object-fit: cover;
-            object-position: center;
-            width: 100%;
-            height: 100%;
-            scale: 1;
-            transition: scale 0.5s ease-in-out, opacity 1s ease-in-out;
-            position: absolute;
-            left: 0;
-            transition: opacity 0.5s ease-in-out, scale 0.5s ease-in-out;
-            background-color: red;
-        }
-        
+
         div.project_details{
             position: absolute;
             bottom: 0em;
@@ -81,18 +68,12 @@ section#catalog{
 }
 
 @media (hover: hover) {
-    section#catalog article:hover img{
-        scale: 1.2;
-    }
-    section#catalog article:hover div.project_details{
-        opacity: 0.8;
-    }
+    section#catalog article:hover img{scale: 1.2;}
+    section#catalog article:hover div.project_details{opacity: 0.8;}
 }
 
 @media (hover: none) {
-    section#catalog article div.project_details{
-        opacity: 0.8;
-    }
+    section#catalog article div.project_details{opacity: 0.8;}
 }
 
 

+ 37 - 28
frontend/src/app/home/home.ts

@@ -1,40 +1,49 @@
 import { Component, OnInit } from '@angular/core';
 import { ProjectService } from '../service/project-service';
+import { UtilService } from '../service/util-service';
 import { ProjectModel } from '../model/project';
 import { environment } from '../../environments/environment';
 
 @Component({ selector: 'app-home', templateUrl: './home.html', styleUrl: './home.scss'})
 export class Home implements OnInit {
-  projects: ProjectModel[] = [];
-  private intervalId: any;
-  apiURL: string = environment.apiUrl;
+    projects: ProjectModel[] = [];
+    private intervalId: any;
+    apiURL: string = environment.apiUrl;
+    utilService: UtilService;
 
-  constructor(private projectService: ProjectService){}
+    constructor(private projectService: ProjectService){
+        this.utilService = new UtilService()
+    }
 
-  ngOnInit(): void {
-    this.projectService.getProjects().subscribe(data => { this.projects = data; });
-    this.startInterval()
-  }
+    ngOnInit(): void {
+        this.projectService.getProjects().subscribe(data => { this.projects = data; });
+        this.startInterval()
+    }
   
-  private delay(ms: number){ return new Promise(resolve => setTimeout(resolve, ms)); }
+    private delay(ms: number){ return new Promise(resolve => setTimeout(resolve, ms)); }
   
-  startInterval() {
-      this.intervalId = setInterval(() => {        
-        const id: number = this.projects[Math.floor(Math.random() * this.projects.length)].id;
-        this.projectService.getProjectRandomImage(id.toString()).subscribe(async image => {
-            var img: HTMLImageElement
-              = <HTMLImageElement>document.getElementById("img-project-" + id);
-            var imgAlt: HTMLImageElement
-              = <HTMLImageElement>document.getElementById("img-project-" + id + "-alt");
-            imgAlt.setAttribute("src", this.apiURL + image.path);
-            await this.delay(1000);
-            img.style.opacity = '0';
-            await this.delay(1000);
-            img.setAttribute("src", "" + imgAlt.getAttribute("src"));
-            img.style.opacity = '1';
-            await this.delay(1000);
-            imgAlt.removeAttribute("src");
-        });
-      }, 2000);
+    startInterval() {
+        this.intervalId = setInterval(() => {
+            const id: number = this.projects[Math.floor(Math.random() * this.projects.length)].id;
+            this.projectService.getProjectRandomImage(id.toString()).subscribe(async image => {
+                var img: HTMLImageElement
+                  = <HTMLImageElement>document.getElementById("img-project-" + id);
+                var imgAlt: HTMLImageElement
+                  = <HTMLImageElement>document.getElementById("img-project-" + id + "-alt");
+                imgAlt.setAttribute("src", this.apiURL + image.path);
+                imgAlt.setAttribute(
+                  "srcset", this.utilService.generateSrcset(this.apiURL + image.path)
+                );
+                await this.delay(1000);
+                img.style.opacity = '0';
+                await this.delay(1000);
+                img.setAttribute("src", "" + imgAlt.getAttribute("src"));
+                img.setAttribute("srcset", "" + imgAlt.getAttribute("srcset"));
+                img.style.opacity = '1';
+                await this.delay(1000);
+                imgAlt.removeAttribute("src");
+                imgAlt.removeAttribute("srcset");
+            });
+        }, 2000);
     }
-}
+}

+ 8 - 8
frontend/src/app/model/project-image.ts

@@ -1,10 +1,10 @@
 export interface ProjectImageModel{
-  id: number;
-  project: number;
-  home: number;
-  path: string;
-  video: number;
-  title: string;
-  description: string;
-  priority: number;
+    id: number;
+    project: number;
+    home: number;
+    path: string;
+    video: number;
+    title: string;
+    description: string;
+    priority: number;
 }

+ 21 - 5
frontend/src/app/project/project.html

@@ -4,6 +4,7 @@
       <img
         (click)=galleryOpen(0); id='img-0' class='img img_main'
         src='{{ this.apiURL }}{{ project.images[0].path }}' alt='{{ project.images[0].title }}'
+        srcset="{{ utilService.generateSrcset(apiURL + project.images[0].path) }}"
         attr.data-description='{{ project.images[0].description }}'
       />
     }
@@ -12,11 +13,26 @@
   </div>
   <div id='images'>
     @for (image of project.images; track image.id) {
-       <img
-         (click)='galleryOpen(image.id)' id='img-{{ image.id }}' class='img'
-         src='{{ this.apiURL }}{{ image.path }}' alt='{{ image.title }}'
-         attr.data-description='{{ image.description }}'
-       />
+      @if (image.video == 0){
+        <img
+          (click)='galleryOpen(image.id)' id='img-{{ image.id }}' class='img'
+          src='{{ this.apiURL }}{{ image.path }}' alt='{{ image.title }}'
+          srcset="{{ utilService.generateSrcset(apiURL + image.path) }}"
+          attr.data-description='{{ image.description }}'
+          data-video='false'
+        />
+      }
+      @else{
+        <video
+          muted (click)='galleryOpen(image.id)' class='img img_vid' id='img-{{ image.id }}'
+          src='{{ this.apiURL }}{{ image.path }}'
+          data-video='true' attr.data-description='{{ image.description }}'
+        >
+        </video>
+        <img (click)='galleryOpen(image.id)' class='video_play'
+          src='/img/icon/video.png' alt='{{ image.title }}'
+        />
+      }
     }
   </div>
   <div id='gallery-cover' onClick='galleryClose();'></div>

+ 27 - 18
frontend/src/app/project/project.scss

@@ -7,6 +7,7 @@ main{
 
 div#details{
     min-height: 24em;
+    margin: auto 4em 0 4em;
    
     p{margin: auto 0.2em;}
 }
@@ -35,6 +36,7 @@ img.video_play{
     height: 3em;
     margin: 0 4.5em 0 -8em;
     z-index: 1;
+    position: relative;
 }
 
 video.img::-webkit-media-controls{display: none;}
@@ -66,6 +68,7 @@ div#gallery-cover{
     background-color: variables.$darker-brown;
     opacity: 0.4;
     transition: opacity 1s ease-in-out;
+    z-index: 2;
 }
 
 div#gallery{
@@ -80,6 +83,7 @@ div#gallery{
     border-radius: 0.4em;
     transition: opacity 1s ease-in-out;
     text-align: center;
+    z-index: 3;
 
     h3{
         text-align: left;
@@ -143,24 +147,29 @@ div#gallery{
         bottom: 2%;
         left: 2%;
         right: 2%;
-    }
-
-    div#gallery img#gallery-img{
-        display: block;
-        max-height: calc(60% - 2em);
-        max-width: 95%;
-        margin: 0.2em auto;
-    }
-    div#gallery p#gallery-text{
-        display: block;
-        max-width: 100%;
-        margin: auto 0.3em;
-        max-height: calc(40% - 2em);
-    }
+        
+        div#gallery-flex{
+            display: block;
+        }
+    
+        img#gallery-img{
+            display: block;
+            max-height: calc(60% - 2em);
+            max-width: 95%;
+            margin: 0.2em auto;
+        }
+    
+        p#gallery-text{
+            display: block;
+            max-width: 100%;
+            margin: auto 0.3em;
+            max-height: calc(40% - 2em);
+        }
 
-    div#gallery div#gallery-controls{
-        position: absolute;
-        bottom: 0;
-        width: 100%;
+        div#gallery-controls{
+            position: absolute;
+            bottom: 0;
+            width: 100%;
+        }
     }
 }

+ 13 - 14
frontend/src/app/project/project.ts

@@ -1,6 +1,7 @@
 import { Component } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { ProjectService } from '../service/project-service';
+import { UtilService } from '../service/util-service';
 import { ProjectModel } from '../model/project';
 import { environment } from '../../environments/environment';
 
@@ -11,6 +12,7 @@ export class Project {
     
     project: ProjectModel | undefined;
     apiURL: string = environment.apiUrl;
+    utilService: UtilService;
     
     /**
      * Index of the photo currently on the gallery.
@@ -22,15 +24,17 @@ export class Project {
      */
     private maxIndex: number = -1;
 
-    constructor(private route: ActivatedRoute, private projectService: ProjectService) { }
+    constructor(private route: ActivatedRoute, private projectService: ProjectService){
+        this.utilService = new UtilService();
+    }
     
     ngOnInit(): void {
-      const id = this.route.snapshot.paramMap.get('id');
-      // Use the id to fetch data or perform actions
-      this.projectService.getProject("" + id).subscribe(data => {
-        this.project = data;
-        this.maxIndex = this.project.images.length; 
-      });
+        const id = this.route.snapshot.paramMap.get('id');
+        // Use the id to fetch data or perform actions
+        this.projectService.getProject("" + id).subscribe(data => {
+            this.project = data;
+            this.maxIndex = this.project.images.length; 
+        });
     }
 
     /**
@@ -39,19 +43,12 @@ export class Project {
      * @param index Index of the photo to display
      */
     galleryOpen(index: any){
-        console.log("GALLERY OPEN" + index);
         let cover: HTMLDivElement = <HTMLDivElement> document.getElementById('gallery-cover');
         let gallery: HTMLDivElement = <HTMLDivElement> document.getElementById('gallery');
         cover.style.display = 'block';
         cover.style.opacity = '0.4';
         gallery.style.display = 'block';
         gallery.style.opacity = '1';
-        // Check index:
-        let i = 0;
-        while (document.getElementById('img-' + i) != null){
-            this.maxIndex = i;
-            i ++
-        }
         if (index >= 0 && index <= this.maxIndex){
             this.curIndex = index;
             this.gallerySet();
@@ -100,6 +97,8 @@ export class Project {
             video.style.display = 'none';
             image.style.display = 'block';
             image.src = document.getElementById('img-' + this.curIndex)?.getAttribute("src") || "";
+            image.srcset
+              = document.getElementById('img-' + this.curIndex)?.getAttribute("srcset") || "";
         }
         let description: string
           = document.getElementById('img-' + this.curIndex)?.dataset["description"] || "";

+ 1 - 0
frontend/src/app/service/project-service.ts

@@ -23,4 +23,5 @@ export class ProjectService {
   getProjectRandomImage(id: string): Observable<ProjectImageModel> {
     return this.http.get<ProjectImageModel>(`${this.apiUrl}/${id}/images/random`);
   }
+  
 }

+ 22 - 0
frontend/src/app/service/util-service.ts

@@ -0,0 +1,22 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+
+/**
+ * Provides utilities to use across the app.
+ */
+export class UtilService {
+    
+    /**
+     * Generates a srcset attribute for an image from it's src.
+     * 
+     * @param src The image src attribute.
+     * @return The computed srcset attribute.
+     */
+    generateSrcset(src: string): string{
+        var srcset: string = "";
+        for (let i = 100; i <= 1000; i += 100) srcset += src + "?w=" + i + " " + i + "w, ";
+        srcset.substring(0, srcset.length - 1);
+        return srcset;
+      }
+}