From f0e45603f4daa00751d1bd2ae676f85820fff7a3 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 19:25:19 +0200 Subject: [PATCH 01/10] Fix jumpy scroll position in large collections --- ui/src/components/NaturalViewer.vue | 2063 ++++++++++++++------------- 1 file changed, 1033 insertions(+), 1030 deletions(-) diff --git a/ui/src/components/NaturalViewer.vue b/ui/src/components/NaturalViewer.vue index 1acd9de..af1a1c0 100644 --- a/ui/src/components/NaturalViewer.vue +++ b/ui/src/components/NaturalViewer.vue @@ -1,1030 +1,1033 @@ - - - - - + + + + + From db6cf64ba39779c85863f9ec04d56b54c0e2f489 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 20:48:02 +0200 Subject: [PATCH 02/10] Add scene load progress spinner --- api.yaml | 3 + internal/layout/album.go | 1 + internal/layout/timeline.go | 1 + internal/openapi/api.gen.go | 3 + internal/render/scene.go | 4 + internal/scene/sceneSource.go | 58 ++--- ui/src/api.js | 320 ++++++++++++++-------------- ui/src/components/NaturalViewer.vue | 49 ++++- ui/src/components/Spinner.vue | 249 ++++++++++++++++++++++ 9 files changed, 500 insertions(+), 188 deletions(-) create mode 100644 ui/src/components/Spinner.vue diff --git a/api.yaml b/api.yaml index 06ea48c..7b62180 100644 --- a/api.yaml +++ b/api.yaml @@ -462,6 +462,9 @@ components: type: integer minimum: 0 example: 506 + loading: + type: boolean + description: True while the scene is loading and the dimensions are not yet known. Collection: type: object diff --git a/internal/layout/album.go b/internal/layout/album.go index dc1a719..d14d1a0 100644 --- a/internal/layout/album.go +++ b/internal/layout/album.go @@ -140,6 +140,7 @@ func LayoutAlbum(layout Layout, collection collection.Collection, scene *render. layoutCounter.Set(index) index++ + scene.FileCount = index } layoutPlaced() diff --git a/internal/layout/timeline.go b/internal/layout/timeline.go index 7b109f9..88e4437 100644 --- a/internal/layout/timeline.go +++ b/internal/layout/timeline.go @@ -126,6 +126,7 @@ func LayoutTimeline(layout Layout, collection collection.Collection, scene *rend layoutCounter.Set(index) index++ + scene.FileCount = index } layoutPlaced() diff --git a/internal/openapi/api.gen.go b/internal/openapi/api.gen.go index ab42a71..3f03a6e 100644 --- a/internal/openapi/api.gen.go +++ b/internal/openapi/api.gen.go @@ -93,6 +93,9 @@ type Scene struct { Bounds *Bounds `json:"bounds,omitempty"` FileCount *int `json:"file_count,omitempty"` Id SceneId `json:"id"` + + // True while the scene is loading and the dimensions are not yet known. + Loading *bool `json:"loading,omitempty"` } // SceneId defines model for SceneId. diff --git a/internal/render/scene.go b/internal/render/scene.go index 3118ede..73fbfba 100644 --- a/internal/render/scene.go +++ b/internal/render/scene.go @@ -54,6 +54,7 @@ type SceneId = string type Scene struct { Id SceneId `json:"id"` CreatedAt time.Time `json:"created_at"` + Loading bool `json:"loading"` Fonts Fonts `json:"-"` Bounds Rect `json:"bounds"` Photos []Photo `json:"-"` @@ -182,6 +183,9 @@ func (scene *Scene) GetRegions(config *Render, bounds Rect, limit *int) []Region if limit != nil { query.Limit = *limit } + if scene.RegionSource == nil { + return []Region{} + } return scene.RegionSource.GetRegionsFromBounds( bounds, scene, diff --git a/internal/scene/sceneSource.go b/internal/scene/sceneSource.go index c7b4ece..8de89c4 100644 --- a/internal/scene/sceneSource.go +++ b/internal/scene/sceneSource.go @@ -64,42 +64,44 @@ func getSceneCost(scene *render.Scene) int64 { return structCost + photosCost + solidsCost + textsCost } -func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Source) render.Scene { - log.Printf("scene loading %v", config.Collection.Id) +func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Source) *render.Scene { - finished := metrics.Elapsed("scene load " + config.Collection.Id) + log.Printf("scene loading %v", config.Collection.Id) scene := source.DefaultScene + scene.CreatedAt = time.Now() + scene.Loading = true - switch config.Layout.Type { - case layout.Timeline: - layout.LayoutTimeline(config.Layout, config.Collection, &scene, imageSource) + go func() { + finished := metrics.Elapsed("scene load " + config.Collection.Id) + switch config.Layout.Type { + case layout.Timeline: + layout.LayoutTimeline(config.Layout, config.Collection, &scene, imageSource) - case layout.Album: - layout.LayoutAlbum(config.Layout, config.Collection, &scene, imageSource) + case layout.Album: + layout.LayoutAlbum(config.Layout, config.Collection, &scene, imageSource) - case layout.Square: - layout.LayoutSquare(&scene, imageSource) + case layout.Square: + layout.LayoutSquare(&scene, imageSource) - case layout.Wall: - layout.LayoutWall(config.Layout, config.Collection, &scene, imageSource) + case layout.Wall: + layout.LayoutWall(config.Layout, config.Collection, &scene, imageSource) - default: - layout.LayoutAlbum(config.Layout, config.Collection, &scene, imageSource) - } - - if scene.RegionSource == nil { - scene.RegionSource = &layout.PhotoRegionSource{ - Source: imageSource, + default: + layout.LayoutAlbum(config.Layout, config.Collection, &scene, imageSource) } - } - - scene.FileCount = len(scene.Photos) - scene.CreatedAt = time.Now() - finished() + if scene.RegionSource == nil { + scene.RegionSource = &layout.PhotoRegionSource{ + Source: imageSource, + } + } + scene.FileCount = len(scene.Photos) + scene.Loading = false + finished() + log.Printf("photos %d, scene %.0f x %.0f\n", len(scene.Photos), scene.Bounds.W, scene.Bounds.H) + }() - log.Printf("photos %d, scene %.0f x %.0f\n", len(scene.Photos), scene.Bounds.W, scene.Bounds.H) - return scene + return &scene } func (source *SceneSource) GetSceneById(id string, imageSource *image.Source) *render.Scene { @@ -181,8 +183,8 @@ func (source *SceneSource) Add(config SceneConfig, imageSource *image.Source) *r scene.Id = id source.scenes.Store(scene.Id, storedScene{ - scene: &scene, + scene: scene, config: config, }) - return &scene + return scene } diff --git a/ui/src/api.js b/ui/src/api.js index e1e65a6..2c32c61 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -1,158 +1,162 @@ -import useSWRV from "swrv"; -import { computed, watch, ref } from "vue"; -import qs from "qs"; - -const host = import.meta.env.VITE_API_HOST || "/api"; - -async function fetcher(endpoint) { - const response = await fetch(host + endpoint); - if (!response.ok) { - console.error(response); - throw new Error(response.statusText); - } - return await response.json(); -} - -export async function get(endpoint, def) { - const response = await fetch(host + endpoint); - if (!response.ok) { - if (def !== undefined) { - return def; - } - console.error(response); - throw new Error(response.statusText); - } - return await response.json(); -} - -export async function post(endpoint, body, def) { - const response = await fetch(host + endpoint, { - method: "POST", - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json; charset=utf-8", - } - }); - if (!response.ok) { - if (def !== undefined) { - return def; - } - console.error(response); - throw new Error(response.statusText); - } - return await response.json(); -} - -export async function getRegions(sceneId, x, y, w, h) { - if (!sceneId) return null; - const response = await get(`/scenes/${sceneId}/regions?x=${x}&y=${y}&w=${w}&h=${h}`); - return response.items; -} - -export async function getRegion(id, sceneParams) { - return get(`/regions/${id}?${sceneParams}`); -} - -export async function getCollections() { - return get(`/collections`); -} - -export async function getCollection(id) { - return get(`/collections/` + id); -} - -export async function createTask(type, id) { - return await post(`/tasks`, { - type, - collection_id: id - }); -} - -export function getTileUrl(sceneId, level, x, y, tileSize, debug) { - const params = { - tile_size: tileSize, - zoom: level, - x, - y, - }; - for (const key in debug) { - if (Object.hasOwnProperty.call(debug, key)) { - if (debug[key]) params[`debug_${key}`] = debug[key]; - } - } - let url = `${host}/scenes/${sceneId}/tiles?${qs.stringify(params)}`; - return url; -} - -export function getFileUrl(id, filename) { - if (!filename) { - return `${host}/files/${id}`; - } - return `${host}/files/${id}/original/${filename}`; -} - -export async function getFileBlob(id) { - return getBlob(`/files/` + id); -} - -export function getThumbnailUrl(id, size, filename) { - return `${host}/files/${id}/image-variants/${size}/${filename}`; -} - -export function getVideoUrl(id, size, filename) { - return `${host}/files/${id}/video-variants/${size}/${filename}`; -} - -export function useApi(getUrl, config) { - const response = useSWRV(getUrl, fetcher, config); - const items = computed(() => response.data.value?.items); - const itemsMutate = async getItems => { - const items = await getItems(); - await response.mutate(() => ({ - items, - })); - }; - return { - ...response, - items, - itemsMutate, - } -} - -export function useTasks() { - const intervalMs = 250; - const response = useApi( - () => `/tasks` - ); - const { items, mutate } = response; - const timer = ref(null); - const resolves = ref([]); - const updateUntilDone = async () => { - await mutate(); - if (resolves.value) { - return new Promise(resolve => resolves.value.push(resolve)); - } - return; - } - watch(items, items => { - if (items.length > 0) { - if (!timer.value) { - timer.value = setTimeout(() => { - timer.value = null; - mutate(); - }, intervalMs); - } - } else { - resolves.value.forEach(resolve => resolve()); - resolves.value.length = 0; - } - }) - return { - ...response, - updateUntilDone, - }; -} - -export async function createScene(params) { - return await post(`/scenes`, params); -} +import useSWRV from "swrv"; +import { computed, watch, ref } from "vue"; +import qs from "qs"; + +const host = import.meta.env.VITE_API_HOST || "/api"; + +async function fetcher(endpoint) { + const response = await fetch(host + endpoint); + if (!response.ok) { + console.error(response); + throw new Error(response.statusText); + } + return await response.json(); +} + +export async function get(endpoint, def) { + const response = await fetch(host + endpoint); + if (!response.ok) { + if (def !== undefined) { + return def; + } + console.error(response); + throw new Error(response.statusText); + } + return await response.json(); +} + +export async function post(endpoint, body, def) { + const response = await fetch(host + endpoint, { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json; charset=utf-8", + } + }); + if (!response.ok) { + if (def !== undefined) { + return def; + } + console.error(response); + throw new Error(response.statusText); + } + return await response.json(); +} + +export async function getRegions(sceneId, x, y, w, h) { + if (!sceneId) return null; + const response = await get(`/scenes/${sceneId}/regions?x=${x}&y=${y}&w=${w}&h=${h}`); + return response.items; +} + +export async function getRegion(id, sceneParams) { + return get(`/regions/${id}?${sceneParams}`); +} + +export async function getCollections() { + return get(`/collections`); +} + +export async function getCollection(id) { + return get(`/collections/` + id); +} + +export async function createTask(type, id) { + return await post(`/tasks`, { + type, + collection_id: id + }); +} + +export function getTileUrl(sceneId, level, x, y, tileSize, debug) { + const params = { + tile_size: tileSize, + zoom: level, + x, + y, + }; + for (const key in debug) { + if (Object.hasOwnProperty.call(debug, key)) { + if (debug[key]) params[`debug_${key}`] = debug[key]; + } + } + let url = `${host}/scenes/${sceneId}/tiles?${qs.stringify(params)}`; + return url; +} + +export function getFileUrl(id, filename) { + if (!filename) { + return `${host}/files/${id}`; + } + return `${host}/files/${id}/original/${filename}`; +} + +export async function getFileBlob(id) { + return getBlob(`/files/` + id); +} + +export function getThumbnailUrl(id, size, filename) { + return `${host}/files/${id}/image-variants/${size}/${filename}`; +} + +export function getVideoUrl(id, size, filename) { + return `${host}/files/${id}/video-variants/${size}/${filename}`; +} + +export function useApi(getUrl, config) { + const response = useSWRV(getUrl, fetcher, config); + const items = computed(() => response.data.value?.items); + const itemsMutate = async getItems => { + if (!getItems) { + await response.mutate(); + return; + } + const items = await getItems(); + await response.mutate(() => ({ + items, + })); + }; + return { + ...response, + items, + itemsMutate, + } +} + +export function useTasks() { + const intervalMs = 250; + const response = useApi( + () => `/tasks` + ); + const { items, mutate } = response; + const timer = ref(null); + const resolves = ref([]); + const updateUntilDone = async () => { + await mutate(); + if (resolves.value) { + return new Promise(resolve => resolves.value.push(resolve)); + } + return; + } + watch(items, items => { + if (items.length > 0) { + if (!timer.value) { + timer.value = setTimeout(() => { + timer.value = null; + mutate(); + }, intervalMs); + } + } else { + resolves.value.forEach(resolve => resolve()); + resolves.value.length = 0; + } + }) + return { + ...response, + updateUntilDone, + }; +} + +export async function createScene(params) { + return await post(`/scenes`, params); +} diff --git a/ui/src/components/NaturalViewer.vue b/ui/src/components/NaturalViewer.vue index af1a1c0..ca3e3a1 100644 --- a/ui/src/components/NaturalViewer.vue +++ b/ui/src/components/NaturalViewer.vue @@ -2,7 +2,7 @@
- + + + + +
{ + if (newValue?.loading) { + let prev = oldValue?.file_count || 0; + if (prev > newValue.file_count) { + prev = 0; + } + sceneLoadFilesPerSecond.value = newValue.file_count - prev; + sceneRefreshTask.perform(); + } else { + sceneLoadFilesPerSecond.value = 0; + } + }) + const sceneRefreshTask = useTask(function*() { + yield timeout(1000); + scenesMutate(); + }).keepLatest() + const regionSeekId = ref(null); const regionSeekApplyTask = useTask(function*(_, router) { @@ -336,6 +366,7 @@ export default { collection, scene, scenes, + sceneLoadFilesPerSecond, region, regionSeekId, regionSeekApplyTask, @@ -420,7 +451,13 @@ export default { }, watch: { scene(newScene, oldScene) { - if (oldScene && newScene && oldScene.id == newScene.id) return; + if ( + oldScene && newScene && + oldScene.id == newScene.id && + oldScene.file_count == newScene.file_count + ) { + return; + } this.$emit("scene", newScene); if (newScene) { this.pushScrollToView(); @@ -976,6 +1013,14 @@ export default { From 08d3b394391d080ceb929c56a59c969144003bcc Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 20:51:24 +0200 Subject: [PATCH 03/10] Skip drawing tiny text --- internal/render/photo.go | 6 ++++-- internal/render/scene.go | 2 +- internal/render/sprite.go | 4 ++-- internal/render/text.go | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/render/photo.go b/internal/render/photo.go index 2094a42..24cbf98 100644 --- a/internal/render/photo.go +++ b/internal/render/photo.go @@ -135,10 +135,11 @@ func (photo *Photo) getBestVariants(config *Render, scene *Scene, c *canvas.Cont func (photo *Photo) Draw(config *Render, scene *Scene, c *canvas.Context, scales Scales, source *image.Source) { pixelArea := photo.Sprite.Rect.GetPixelArea(c, image.Size{X: 1, Y: 1}) - path := photo.GetPath(source) if pixelArea < config.MaxSolidPixelArea { style := c.Style + // TODO: this can be a bottleneck for lots of images + // if it ends up hitting the database for each individual image info := source.GetInfo(photo.Id) style.FillColor = info.GetColor() @@ -147,6 +148,7 @@ func (photo *Photo) Draw(config *Render, scene *Scene, c *canvas.Context, scales } drawn := false + path := photo.GetPath(source) variants := photo.getBestVariants(config, scene, c, scales, source, path) for _, variant := range variants { // text := fmt.Sprintf("index %d zd %4.2f %s", index, bitmapAtZoom.ZoomDist, bitmap.Path) @@ -193,7 +195,7 @@ func (photo *Photo) Draw(config *Render, scene *Scene, c *canvas.Context, scales text := fmt.Sprintf("%dx%d %s", bounds.Size().X, bounds.Size().Y, variant.String()) font := scene.Fonts.Debug font.Color = canvas.Lime - bitmap.Sprite.DrawText(c, scales, &font, text) + bitmap.Sprite.DrawText(config, c, scales, &font, text) } break diff --git a/internal/render/scene.go b/internal/render/scene.go index 73fbfba..299282e 100644 --- a/internal/render/scene.go +++ b/internal/render/scene.go @@ -127,7 +127,7 @@ func (scene *Scene) Draw(config *Render, c *canvas.Context, scales Scales, sourc for i := range scene.Texts { text := &scene.Texts[i] - text.Draw(c, scales) + text.Draw(config, c, scales) } } diff --git a/internal/render/sprite.go b/internal/render/sprite.go index 79c177d..b624021 100644 --- a/internal/render/sprite.go +++ b/internal/render/sprite.go @@ -66,9 +66,9 @@ func (sprite *Sprite) DrawWithStyle(c *canvas.Context, style canvas.Style) { ) } -func (sprite *Sprite) DrawText(c *canvas.Context, scales Scales, font *canvas.FontFace, txt string) { +func (sprite *Sprite) DrawText(config *Render, c *canvas.Context, scales Scales, font *canvas.FontFace, txt string) { text := NewTextFromRect(sprite.Rect, font, txt) - text.Draw(c, scales) + text.Draw(config, c, scales) } func (sprite *Sprite) IsVisible(c *canvas.Context, scales Scales) bool { diff --git a/internal/render/text.go b/internal/render/text.go index 8ecde40..b461a99 100644 --- a/internal/render/text.go +++ b/internal/render/text.go @@ -1,6 +1,10 @@ package render -import "github.com/tdewolff/canvas" +import ( + "photofield/internal/image" + + "github.com/tdewolff/canvas" +) type Text struct { Sprite Sprite @@ -16,8 +20,14 @@ func NewTextFromRect(rect Rect, font *canvas.FontFace, txt string) Text { return text } -func (text *Text) Draw(c *canvas.Context, scales Scales) { +func (text *Text) Draw(config *Render, c *canvas.Context, scales Scales) { if text.Sprite.IsVisible(c, scales) { + pixelArea := text.Sprite.Rect.GetPixelArea(c, image.Size{X: 1, Y: 1}) + if pixelArea < config.MaxSolidPixelArea { + // Skip rendering small text + return + } + textLine := canvas.NewTextLine(*text.Font, text.Text, canvas.Left) c.RenderText(textLine, c.View().Mul(text.Sprite.Rect.GetMatrix())) } From 026d28e6c0167de97d22b3a6eca06d796bdf7215 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 20:52:42 +0200 Subject: [PATCH 04/10] Fix window resizing --- ui/src/components/NaturalViewer.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/components/NaturalViewer.vue b/ui/src/components/NaturalViewer.vue index ca3e3a1..a4dab06 100644 --- a/ui/src/components/NaturalViewer.vue +++ b/ui/src/components/NaturalViewer.vue @@ -528,10 +528,12 @@ export default { addFullpageListeners() { window.addEventListener('scroll', this.onScroll); + window.addEventListener('resize', this.onWindowResize); }, removeFullpageListeners() { window.removeEventListener('scroll', this.onScroll); + window.removeEventListener('resize', this.onWindowResize); }, attachScrollbar(scrollbar) { @@ -650,10 +652,14 @@ export default { }, onResize(rect) { + if (rect.width == 0 || rect.height == 0) return; + this.resizeApplyTask.perform(rect, this.pushScrollToView); + }, + + onWindowResize(rect) { if (rect.width == 0 || rect.height == 0) return; const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); - this.resizeApplyTask.perform(rect, this.pushScrollToView); }, onScroll(event) { From 135d4147fc1518d51100e363c6a3b5bc81ceb981 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 20:55:02 +0200 Subject: [PATCH 05/10] Use same tile size for region menu --- ui/src/components/NaturalViewer.vue | 1 + ui/src/components/RegionMenu.vue | 412 ++++++++++++++-------------- 2 files changed, 207 insertions(+), 206 deletions(-) diff --git a/ui/src/components/NaturalViewer.vue b/ui/src/components/NaturalViewer.vue index a4dab06..d0b23fe 100644 --- a/ui/src/components/NaturalViewer.vue +++ b/ui/src/components/NaturalViewer.vue @@ -65,6 +65,7 @@ :sceneParams="sceneParams" :flipX="contextFlipX" :flipY="contextFlipY" + :tileSize="tileSize" @close="closeContextMenu()" > diff --git a/ui/src/components/RegionMenu.vue b/ui/src/components/RegionMenu.vue index bcb6c0e..dbae63d 100644 --- a/ui/src/components/RegionMenu.vue +++ b/ui/src/components/RegionMenu.vue @@ -1,207 +1,207 @@ - - - - - \ No newline at end of file From d7bed01ae4e0e01418bce09df943a151c067884e Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 20:56:27 +0200 Subject: [PATCH 06/10] Limit minimum tiled image size --- ui/src/components/TileViewer.vue | 729 ++++++++++++++++--------------- 1 file changed, 374 insertions(+), 355 deletions(-) diff --git a/ui/src/components/TileViewer.vue b/ui/src/components/TileViewer.vue index 4a23d3d..530e96f 100644 --- a/ui/src/components/TileViewer.vue +++ b/ui/src/components/TileViewer.vue @@ -1,355 +1,374 @@ - - - - - + + + + + From 058dc38b4485b81142de0cc59f70f7414e9a1691 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Sun, 10 Jul 2022 21:10:23 +0200 Subject: [PATCH 07/10] Better image/video variant handling --- api.yaml | 21 +--- defaults.yaml | 190 ++++++++++++++++-------------- internal/image/image.go | 15 +-- internal/image/source.go | 30 ++++- internal/image/thumbnail.go | 4 +- internal/layout/common.go | 34 +++--- internal/openapi/api.gen.go | 62 +--------- internal/render/photo.go | 52 +------- main.go | 51 ++------ ui/src/api.js | 6 +- ui/src/components/RegionMenu.vue | 2 +- ui/src/components/TileViewer.vue | 8 +- ui/src/components/VideoPlayer.vue | 12 +- 13 files changed, 183 insertions(+), 304 deletions(-) diff --git a/api.yaml b/api.yaml index 7b62180..5ca59a4 100644 --- a/api.yaml +++ b/api.yaml @@ -311,25 +311,10 @@ paths: "404": $ref: "#/components/responses/FileNotFound" - /files/{id}/image-variants/{size}/{filename}: + /files/{id}/variants/{size}/{filename}: get: - description: Get an image thumbnail of the specified predefined size and - with an arbitrary filename as part of the URL - tags: ["Files"] - parameters: - - $ref: "#/components/parameters/FileIdPathParam" - - $ref: "#/components/parameters/FilenamePathParam" - - $ref: "#/components/parameters/SizePathParam" - responses: - "200": - $ref: "#/components/responses/FileResponse" - "404": - $ref: "#/components/responses/FileNotFound" - - /files/{id}/video-variants/{size}/{filename}: - get: - description: Get a resized video of the specified predefined size and - with an arbitrary filename as part of the URL + description: Get an image or resized video variant/thumbnail of the + specified predefined size and with an arbitrary filename as part of the URL tags: ["Files"] parameters: - $ref: "#/components/parameters/FileIdPathParam" diff --git a/defaults.yaml b/defaults.yaml index 142d121..2da9630 100644 --- a/defaults.yaml +++ b/defaults.yaml @@ -60,96 +60,104 @@ media: # Extensions to use to understand a file to be an image extensions: [".jpg", ".jpeg", ".png", ".gif"] - # Pre-generated thumbnail configuration, these thumbnails will be used to - # greatly speed up the rendering - thumbnails: - - # name: Short thumbnail type name - # - # path: Path template where to find the thumbnail. - # {{.Dir}} is replaced by the parent directory of the original photo - # {{.Filename}} is replaced by the original photo filename - # - # fit: Aspect ratio fit of the thumbnail in case it doesn't match the - # original photo. - # - # INSIDE - # The thumbnail size is the maximum size of each dimension, so in case - # of different aspect ratios, one dimension will always be smaller. - # - # OUTSIDE - # The thumbnail size is the minimum size of each dimension, so in case - # of different aspect ratios, one dimension will always be bigger. - # - # ORIGINAL - # The size of the thumbnail is equal the size of the original. Mostly - # useful for transcoded or differently encoded files. - # - # width - # height: Predefined thumbnail dimensions used to pick the most - # appropriately-sized thumbnail for the zoom level. - # - # extra_cost: Additional weight to add when picking the closest thumbnail. - # Higher number means that other thumbnails will be preferred - # if they exist. - - # - # Embedded JPEG thumbnail - # - - name: exif-thumb - exif: ThumbnailImage - fit: INSIDE - width: 120 - height: 120 - # It's expensive to extract, so this makes it more of a last resort, but - # still a lot better than loading the original photo. - extra_cost: 10 - - # - # Synology Moments / Photo Station thumbnails - # - - name: S - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_S.jpg" - fit: INSIDE - width: 120 - height: 120 - - - name: SM - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_SM.jpg" - fit: OUTSIDE - width: 240 - height: 240 - - - name: M - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_M.jpg" - fit: OUTSIDE - width: 320 - height: 320 - - - name: B - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_B.jpg" - fit: INSIDE - width: 640 - height: 640 - - - name: XL - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_XL.jpg" - fit: OUTSIDE - width: 1280 - height: 1280 - videos: extensions: [".mp4"] - thumbnails: - # - # Synology Moments / Photo Station video variants - # - - name: M - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_FILM_M.mp4" - fit: OUTSIDE - width: 720 - height: 720 - - - name: H264 - path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_FILM_H264.mp4" - fit: ORIGINAL + + # Pre-generated thumbnail configuration, these thumbnails will be used to + # greatly speed up the rendering + thumbnails: + + # name: Short thumbnail type name + # + # path: Path template where to find the thumbnail. + # {{.Dir}} is replaced by the parent directory of the original photo + # {{.Filename}} is replaced by the original photo filename + # + # fit: Aspect ratio fit of the thumbnail in case it doesn't match the + # original photo. + # + # INSIDE + # The thumbnail size is the maximum size of each dimension, so in case + # of different aspect ratios, one dimension will always be smaller. + # + # OUTSIDE + # The thumbnail size is the minimum size of each dimension, so in case + # of different aspect ratios, one dimension will always be bigger. + # + # ORIGINAL + # The size of the thumbnail is equal the size of the original. Mostly + # useful for transcoded or differently encoded files. + # + # width + # height: Predefined thumbnail dimensions used to pick the most + # appropriately-sized thumbnail for the zoom level. + # + # extra_cost: Additional weight to add when picking the closest thumbnail. + # Higher number means that other thumbnails will be preferred + # if they exist. + + # + # Embedded JPEG thumbnail + # + - name: exif-thumb + exif: ThumbnailImage + extensions: [".jpg", ".jpeg"] + fit: INSIDE + width: 120 + height: 120 + # It's expensive to extract, so this makes it more of a last resort, but + # still a lot better than loading the original photo. + extra_cost: 10 + + # + # Synology Moments / Photo Station thumbnails + # + - name: S + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_S.jpg" + extensions: [".jpg", ".jpeg", ".png", ".gif", ".mp4"] + fit: INSIDE + width: 120 + height: 120 + + - name: SM + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_SM.jpg" + extensions: [".jpg", ".jpeg", ".png", ".gif", ".mp4"] + fit: OUTSIDE + width: 240 + height: 240 + + - name: M + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_M.jpg" + extensions: [".jpg", ".jpeg", ".png", ".gif", ".mp4"] + fit: OUTSIDE + width: 320 + height: 320 + + - name: B + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_B.jpg" + extensions: [".jpg", ".jpeg", ".png", ".gif"] + fit: INSIDE + width: 640 + height: 640 + + - name: XL + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_XL.jpg" + extensions: [".jpg", ".jpeg", ".png", ".gif", ".mp4"] + fit: OUTSIDE + width: 1280 + height: 1280 + + # + # Synology Moments / Photo Station video variants + # + - name: FM + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_FILM_M.mp4" + extensions: [".mp4"] + fit: OUTSIDE + width: 720 + height: 720 + + - name: H264 + path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_FILM_H264.mp4" + extensions: [".mp4"] + fit: ORIGINAL diff --git a/internal/image/image.go b/internal/image/image.go index 24c2e6b..fad79d3 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -115,20 +115,9 @@ func (source *Source) GetImageOrThumbnail(path string, thumbnail *Thumbnail) (im return source.imageCache.GetOrLoad(path, thumbnail, source) } -func (source *Source) GetSmallestThumbnail(path string) string { - for i := range source.Images.Thumbnails { - thumbnail := &source.Images.Thumbnails[i] - thumbnailPath := thumbnail.GetPath(path) - if source.Exists(thumbnailPath) { - return thumbnailPath - } - } - return "" -} - func (source *Source) LoadSmallestImage(path string) (image.Image, error) { - for i := range source.Images.Thumbnails { - thumbnail := &source.Images.Thumbnails[i] + for i := range source.Thumbnails { + thumbnail := &source.Thumbnails[i] thumbnailPath := thumbnail.GetPath(path) file, err := os.Open(thumbnailPath) if err != nil { diff --git a/internal/image/source.go b/internal/image/source.go index 1599308..f9bec93 100644 --- a/internal/image/source.go +++ b/internal/image/source.go @@ -47,17 +47,17 @@ type Config struct { ConcurrentMetaLoads int `json:"concurrent_meta_loads"` ConcurrentColorLoads int `json:"concurrent_color_loads"` - ListExtensions []string `json:"extensions"` - DateFormats []string `json:"date_formats"` - Images FileConfig `json:"images"` - Videos FileConfig `json:"videos"` + ListExtensions []string `json:"extensions"` + DateFormats []string `json:"date_formats"` + Images FileConfig `json:"images"` + Videos FileConfig `json:"videos"` + Thumbnails []Thumbnail `json:"thumbnails"` Caches Caches `json:"caches"` } type FileConfig struct { - Extensions []string `json:"extensions"` - Thumbnails []Thumbnail `json:"thumbnails"` + Extensions []string `json:"extensions"` } type Source struct { @@ -210,3 +210,21 @@ func (source *Source) GetDir(dir string) Info { result, _ := source.database.GetDir(dir) return result.Info } + +func (source *Source) GetApplicableThumbnails(path string) []Thumbnail { + thumbs := make([]Thumbnail, 0, len(source.Thumbnails)) + pathExt := strings.ToLower(filepath.Ext(path)) + for _, t := range source.Thumbnails { + supported := false + for _, ext := range t.Extensions { + if pathExt == ext { + supported = true + break + } + } + if supported { + thumbs = append(thumbs, t) + } + } + return thumbs +} diff --git a/internal/image/thumbnail.go b/internal/image/thumbnail.go index ad4402d..d508c77 100644 --- a/internal/image/thumbnail.go +++ b/internal/image/thumbnail.go @@ -17,8 +17,8 @@ type Thumbnail struct { Name string `json:"name"` PathTemplateRaw string `json:"path"` PathTemplate *template.Template - - Exif string `json:"exif"` + Exif string `json:"exif"` + Extensions []string `json:"extensions"` SizeTypeRaw string `json:"fit"` SizeType ThumbnailSizeType diff --git a/internal/layout/common.go b/internal/layout/common.go index a6a1323..8981be0 100644 --- a/internal/layout/common.go +++ b/internal/layout/common.go @@ -1,6 +1,7 @@ package layout import ( + "fmt" "log" "path/filepath" "photofield/internal/image" @@ -48,9 +49,10 @@ type PhotoRegionSource struct { } type RegionThumbnail struct { - Name string `json:"name"` - Width int `json:"width"` - Height int `json:"height"` + Name string `json:"name"` + Width int `json:"width"` + Height int `json:"height"` + Filename string `json:"filename"` } type PhotoRegionData struct { @@ -77,24 +79,26 @@ func (regionSource PhotoRegionSource) getRegionFromPhoto(id int, photo *render.P Y: info.Height, } isVideo := source.IsSupportedVideo(originalPath) + extension := strings.ToLower(filepath.Ext(originalPath)) + filename := filepath.Base(originalPath) - var thumbnailTemplates []image.Thumbnail - if isVideo { - thumbnailTemplates = source.Videos.Thumbnails - } else { - thumbnailTemplates = source.Images.Thumbnails - } - + thumbnailTemplates := source.GetApplicableThumbnails(originalPath) var thumbnails []RegionThumbnail for i := range thumbnailTemplates { thumbnail := &thumbnailTemplates[i] thumbnailPath := thumbnail.GetPath(originalPath) if source.Exists(thumbnailPath) { thumbnailSize := thumbnail.Fit(originalSize) + basename := strings.TrimSuffix(filename, extension) + thumbnailFilename := fmt.Sprintf( + "%s_%s%s", + basename, thumbnail.Name, filepath.Ext(thumbnailPath), + ) thumbnails = append(thumbnails, RegionThumbnail{ - Name: thumbnail.Name, - Width: thumbnailSize.X, - Height: thumbnailSize.Y, + Name: thumbnail.Name, + Width: thumbnailSize.X, + Height: thumbnailSize.Y, + Filename: thumbnailFilename, }) } } @@ -105,8 +109,8 @@ func (regionSource PhotoRegionSource) getRegionFromPhoto(id int, photo *render.P Data: PhotoRegionData{ Id: int(photo.Id), Path: originalPath, - Filename: filepath.Base(originalPath), - Extension: strings.ToLower(filepath.Ext(originalPath)), + Filename: filename, + Extension: extension, Video: isVideo, Width: info.Width, Height: info.Height, diff --git a/internal/openapi/api.gen.go b/internal/openapi/api.gen.go index 3f03a6e..9a951e0 100644 --- a/internal/openapi/api.gen.go +++ b/internal/openapi/api.gen.go @@ -208,14 +208,11 @@ type ServerInterface interface { // (GET /files/{id}) GetFilesId(w http.ResponseWriter, r *http.Request, id FileIdPathParam) - // (GET /files/{id}/image-variants/{size}/{filename}) - GetFilesIdImageVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id FileIdPathParam, size SizePathParam, filename FilenamePathParam) - // (GET /files/{id}/original/{filename}) GetFilesIdOriginalFilename(w http.ResponseWriter, r *http.Request, id FileIdPathParam, filename FilenamePathParam) - // (GET /files/{id}/video-variants/{size}/{filename}) - GetFilesIdVideoVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id FileIdPathParam, size SizePathParam, filename FilenamePathParam) + // (GET /files/{id}/variants/{size}/{filename}) + GetFilesIdVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id FileIdPathParam, size SizePathParam, filename FilenamePathParam) // (GET /scenes) GetScenes(w http.ResponseWriter, r *http.Request, params GetScenesParams) @@ -317,50 +314,6 @@ func (siw *ServerInterfaceWrapper) GetFilesId(w http.ResponseWriter, r *http.Req handler(w, r.WithContext(ctx)) } -// GetFilesIdImageVariantsSizeFilename operation middleware -func (siw *ServerInterfaceWrapper) GetFilesIdImageVariantsSizeFilename(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var err error - - // ------------- Path parameter "id" ------------- - var id FileIdPathParam - - err = runtime.BindStyledParameter("simple", false, "id", chi.URLParam(r, "id"), &id) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid format for parameter id: %s", err), http.StatusBadRequest) - return - } - - // ------------- Path parameter "size" ------------- - var size SizePathParam - - err = runtime.BindStyledParameter("simple", false, "size", chi.URLParam(r, "size"), &size) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid format for parameter size: %s", err), http.StatusBadRequest) - return - } - - // ------------- Path parameter "filename" ------------- - var filename FilenamePathParam - - err = runtime.BindStyledParameter("simple", false, "filename", chi.URLParam(r, "filename"), &filename) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid format for parameter filename: %s", err), http.StatusBadRequest) - return - } - - var handler = func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetFilesIdImageVariantsSizeFilename(w, r, id, size, filename) - } - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler(w, r.WithContext(ctx)) -} - // GetFilesIdOriginalFilename operation middleware func (siw *ServerInterfaceWrapper) GetFilesIdOriginalFilename(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -396,8 +349,8 @@ func (siw *ServerInterfaceWrapper) GetFilesIdOriginalFilename(w http.ResponseWri handler(w, r.WithContext(ctx)) } -// GetFilesIdVideoVariantsSizeFilename operation middleware -func (siw *ServerInterfaceWrapper) GetFilesIdVideoVariantsSizeFilename(w http.ResponseWriter, r *http.Request) { +// GetFilesIdVariantsSizeFilename operation middleware +func (siw *ServerInterfaceWrapper) GetFilesIdVariantsSizeFilename(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error @@ -430,7 +383,7 @@ func (siw *ServerInterfaceWrapper) GetFilesIdVideoVariantsSizeFilename(w http.Re } var handler = func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetFilesIdVideoVariantsSizeFilename(w, r, id, size, filename) + siw.Handler.GetFilesIdVariantsSizeFilename(w, r, id, size, filename) } for _, middleware := range siw.HandlerMiddlewares { @@ -889,14 +842,11 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/files/{id}", wrapper.GetFilesId) }) - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/files/{id}/image-variants/{size}/{filename}", wrapper.GetFilesIdImageVariantsSizeFilename) - }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/files/{id}/original/{filename}", wrapper.GetFilesIdOriginalFilename) }) r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/files/{id}/video-variants/{size}/{filename}", wrapper.GetFilesIdVideoVariantsSizeFilename) + r.Get(options.BaseURL+"/files/{id}/variants/{size}/{filename}", wrapper.GetFilesIdVariantsSizeFilename) }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/scenes", wrapper.GetScenes) diff --git a/internal/render/photo.go b/internal/render/photo.go index 24cbf98..eafc088 100644 --- a/internal/render/photo.go +++ b/internal/render/photo.go @@ -53,51 +53,6 @@ func (photo *Photo) Place(x float64, y float64, width float64, height float64, s photo.Sprite.PlaceFit(x, y, width, height, imageWidth, imageHeight) } -func (photo *Photo) getBestBitmaps(config *Render, scene *Scene, c *canvas.Context, scales Scales, source *image.Source) []BitmapAtZoom { - - originalInfo := photo.GetInfo(source) - originalSize := originalInfo.Size() - originalPath := photo.GetPath(source) - originalZoomDist := math.Inf(1) - if source.IsSupportedImage(originalPath) { - originalZoomDist = photo.Sprite.Rect.GetPixelZoomDist(c, originalSize) - } - - bitmaps := make([]BitmapAtZoom, 1+len(source.Images.Thumbnails)) - bitmaps[0] = BitmapAtZoom{ - Bitmap: Bitmap{ - Path: originalPath, - Orientation: originalInfo.Orientation, - Sprite: photo.Sprite, - }, - ZoomDist: originalZoomDist, - } - - for i := range source.Images.Thumbnails { - thumbnail := &source.Images.Thumbnails[i] - thumbSize := thumbnail.Fit(originalSize) - thumbPath := thumbnail.GetPath(originalPath) - bitmaps[1+i] = BitmapAtZoom{ - Bitmap: Bitmap{ - Path: thumbPath, - Sprite: Sprite{ - Rect: photo.Sprite.Rect, - }, - }, - ZoomDist: photo.Sprite.Rect.GetPixelZoomDist(c, thumbSize), - } - // fmt.Printf("orig w %4.0f h %4.0f thumb w %4.0f h %4.0f zoom dist best %8.2f cur %8.2f area %8.6f\n", originalSize.Width, originalSize.Height, thumbSize.Width, thumbSize.Height, bestZoomDist, zoomDist, photo.Original.Sprite.Rect.GetPixelArea(c, thumbSize)) - } - - sort.Slice(bitmaps, func(i, j int) bool { - a := bitmaps[i] - b := bitmaps[j] - return a.ZoomDist < b.ZoomDist - }) - - return bitmaps -} - func (photo *Photo) getBestVariants(config *Render, scene *Scene, c *canvas.Context, scales Scales, source *image.Source, originalPath string) []Variant { originalInfo := photo.GetInfo(source) @@ -107,15 +62,16 @@ func (photo *Photo) getBestVariants(config *Render, scene *Scene, c *canvas.Cont originalZoomDist = photo.Sprite.Rect.GetPixelZoomDist(c, originalSize) } - variants := make([]Variant, 1+len(source.Images.Thumbnails)) + thumbnails := source.GetApplicableThumbnails(originalPath) + variants := make([]Variant, 1+len(thumbnails)) variants[0] = Variant{ Thumbnail: nil, Orientation: originalInfo.Orientation, ZoomDist: originalZoomDist, } - for i := range source.Images.Thumbnails { - thumbnail := &source.Images.Thumbnails[i] + for i := range thumbnails { + thumbnail := &thumbnails[i] thumbSize := thumbnail.Fit(originalSize) variants[1+i] = Variant{ Thumbnail: thumbnail, diff --git a/main.go b/main.go index a6765fc..8a5e03a 100644 --- a/main.go +++ b/main.go @@ -681,17 +681,18 @@ func (*Api) GetFilesIdOriginalFilename(w http.ResponseWriter, r *http.Request, i http.ServeFile(w, r, path) } -func (*Api) GetFilesIdImageVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id openapi.FileIdPathParam, size openapi.SizePathParam, filename openapi.FilenamePathParam) { +func (*Api) GetFilesIdVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id openapi.FileIdPathParam, size openapi.SizePathParam, filename openapi.FilenamePathParam) { imagePath, err := imageSource.GetImagePath(image.ImageId(id)) if err == image.ErrNotFound { - problem(w, r, http.StatusNotFound, "Image not found") + problem(w, r, http.StatusNotFound, "Path not found") return } path := "" - for i := range imageSource.Images.Thumbnails { - thumbnail := imageSource.Images.Thumbnails[i] + thumbnails := imageSource.GetApplicableThumbnails(imagePath) + for i := range thumbnails { + thumbnail := thumbnails[i] candidatePath := thumbnail.GetPath(imagePath) if !imageSource.Exists(candidatePath) { continue @@ -703,40 +704,7 @@ func (*Api) GetFilesIdImageVariantsSizeFilename(w http.ResponseWriter, r *http.R } if path == "" || !imageSource.Exists(path) { - problem(w, r, http.StatusNotFound, "Thumbnail not found") - return - } - - http.ServeFile(w, r, path) -} - -func (*Api) GetFilesIdVideoVariantsSizeFilename(w http.ResponseWriter, r *http.Request, id openapi.FileIdPathParam, size openapi.SizePathParam, filename openapi.FilenamePathParam) { - - // if size == "thumb" { - // size = "M" - // } - - videoPath, err := imageSource.GetImagePath(image.ImageId(id)) - if err == image.ErrNotFound { - problem(w, r, http.StatusNotFound, "Video not found") - return - } - - path := "" - for i := range imageSource.Videos.Thumbnails { - thumbnail := imageSource.Videos.Thumbnails[i] - candidatePath := thumbnail.GetPath(videoPath) - if !imageSource.Exists(candidatePath) { - continue - } - if size != "full" && thumbnail.Name != string(size) { - continue - } - path = candidatePath - } - - if path == "" || !imageSource.Exists(path) { - problem(w, r, http.StatusNotFound, "Resized video not found") + problem(w, r, http.StatusNotFound, "Variant not found") return } @@ -857,11 +825,8 @@ func loadConfiguration(path string) AppConfig { } } - for i := range appConfig.Media.Images.Thumbnails { - appConfig.Media.Images.Thumbnails[i].Init() - } - for i := range appConfig.Media.Videos.Thumbnails { - appConfig.Media.Videos.Thumbnails[i].Init() + for i := range appConfig.Media.Thumbnails { + appConfig.Media.Thumbnails[i].Init() } return appConfig diff --git a/ui/src/api.js b/ui/src/api.js index 2c32c61..47cc809 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -96,11 +96,7 @@ export async function getFileBlob(id) { } export function getThumbnailUrl(id, size, filename) { - return `${host}/files/${id}/image-variants/${size}/${filename}`; -} - -export function getVideoUrl(id, size, filename) { - return `${host}/files/${id}/video-variants/${size}/${filename}`; + return `${host}/files/${id}/variants/${size}/${filename}`; } export function useApi(getUrl, config) { diff --git a/ui/src/components/RegionMenu.vue b/ui/src/components/RegionMenu.vue index dbae63d..69fbfae 100644 --- a/ui/src/components/RegionMenu.vue +++ b/ui/src/components/RegionMenu.vue @@ -46,7 +46,7 @@ v-for="thumb in region.data?.thumbnails" :key="thumb.name" class="thumbnail" - :href="getThumbnailUrl(region.data.id, thumb.name, region.data.filename)" + :href="getThumbnailUrl(region.data.id, thumb.name, thumb.filename)" target="_blank" > {{ thumb.width }} diff --git a/ui/src/components/TileViewer.vue b/ui/src/components/TileViewer.vue index 530e96f..1da9fb4 100644 --- a/ui/src/components/TileViewer.vue +++ b/ui/src/components/TileViewer.vue @@ -44,7 +44,13 @@ export default { watch: { scene(newScene, oldScene) { - if (newScene?.id == oldScene?.id) return; + if ( + newScene?.id == oldScene?.id && + newScene.bounds.w == oldScene.bounds.w && + newScene.bounds.h == oldScene.bounds.h + ) { + return; + } this.reset(); }, diff --git a/ui/src/components/VideoPlayer.vue b/ui/src/components/VideoPlayer.vue index 7f12651..972bc02 100644 --- a/ui/src/components/VideoPlayer.vue +++ b/ui/src/components/VideoPlayer.vue @@ -19,7 +19,7 @@