Explorar o código

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

Iñigo Valentin hai 10 meses
pai
achega
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;
+      }
+}