diff --git a/README.md b/README.md index e9de7e6..00f3789 100644 --- a/README.md +++ b/README.md @@ -247,14 +247,9 @@ quirks that exist in the current version. * `Escape` or pinch out to get back to the list of photos * Zoom in/out directly with `Ctrl/Cmd`+`Wheel` * Pinch-to-zoom on touch devices - * ⚠ You have to currently tap to zoom first - this is undesirable behavior, but - fixing it is a little tricky right now. * Press/hold `Arrow Left` or `Arrow Right` to quickly switch between photos - * There is currently no other easy way to do this and that's not great, - especially on touch-only devices. -* Right-click or long-tap as usual to open context menu. It's custom due to the - fact that the native one is not useful for common use-cases as the images - aren't loaded individually. +* Right-click or long-tap as usual to open a custom context menu allowing you to + copy or download original photos or thumbnails. ![context menu](docs/assets/context-menu.png) diff --git a/api.yaml b/api.yaml index 3b7040e..582927f 100644 --- a/api.yaml +++ b/api.yaml @@ -81,10 +81,15 @@ paths: schema: $ref: "#/components/schemas/CollectionId" - - name: scene_width + - name: viewport_width in: query schema: - $ref: "#/components/schemas/SceneWidth" + $ref: "#/components/schemas/ViewportWidth" + + - name: viewport_height + in: query + schema: + $ref: "#/components/schemas/ViewportHeight" - name: image_height in: query @@ -159,6 +164,12 @@ paths: minimum: 1 example: 256 + - name: background_color + in: query + schema: + type: string + example: "#000000" + - name: zoom in: query required: true @@ -593,14 +604,16 @@ components: type: object required: - collection_id - - scene_width - - image_height + - viewport_width + - viewport_height - layout properties: collection_id: $ref: "#/components/schemas/CollectionId" - scene_width: - $ref: "#/components/schemas/SceneWidth" + viewport_width: + $ref: "#/components/schemas/ViewportWidth" + viewport_height: + $ref: "#/components/schemas/ViewportHeight" image_height: $ref: "#/components/schemas/ImageHeight" layout: @@ -628,11 +641,21 @@ components: type: string example: Tqcqtc6h69 - SceneWidth: + ViewportWidth: type: number minimum: 0 example: 1200 + ViewportHeight: + type: number + minimum: 0 + example: 800 + + ImageWidth: + type: number + minimum: 0 + example: 400 + ImageHeight: type: number minimum: 0 @@ -677,6 +700,7 @@ components: - ALBUM - SQUARE - WALL + - STRIP Problem: type: object diff --git a/internal/image/source.go b/internal/image/source.go index 72bd948..1de1904 100644 --- a/internal/image/source.go +++ b/internal/image/source.go @@ -42,6 +42,17 @@ type SimilarityInfo struct { Similarity float32 } +func SimilarityInfosToSourcedInfos(sinfos <-chan SimilarityInfo) <-chan SourcedInfo { + out := make(chan SourcedInfo) + go func() { + for sinfo := range sinfos { + out <- sinfo.SourcedInfo + } + close(out) + }() + return out +} + type CacheConfig struct { MaxSize string `json:"max_size"` } diff --git a/internal/layout/album.go b/internal/layout/album.go index 1fb7d15..590429e 100644 --- a/internal/layout/album.go +++ b/internal/layout/album.go @@ -86,7 +86,7 @@ func LayoutAlbum(layout Layout, collection collection.Collection, scene *render. sceneMargin := 10. - scene.Bounds.W = layout.SceneWidth + scene.Bounds.W = layout.ViewportWidth event := AlbumEvent{ First: true, @@ -96,7 +96,7 @@ func LayoutAlbum(layout Layout, collection collection.Collection, scene *render. rect := render.Rect{ X: sceneMargin, - Y: sceneMargin, + Y: sceneMargin + 64, W: scene.Bounds.W - sceneMargin*2, H: 0, } diff --git a/internal/layout/common.go b/internal/layout/common.go index 2f98159..6537a83 100644 --- a/internal/layout/common.go +++ b/internal/layout/common.go @@ -18,14 +18,16 @@ const ( Square Type = "SQUARE" Wall Type = "WALL" Search Type = "SEARCH" + Strip Type = "STRIP" ) type Layout struct { - Type Type `json:"type"` - SceneWidth float64 - ImageHeight float64 - ImageSpacing float64 - LineSpacing float64 + Type Type `json:"type"` + ViewportWidth float64 + ViewportHeight float64 + ImageHeight float64 + ImageSpacing float64 + LineSpacing float64 } type Section struct { diff --git a/internal/layout/search.go b/internal/layout/search.go index 1594364..c6f9cf8 100644 --- a/internal/layout/search.go +++ b/internal/layout/search.go @@ -29,7 +29,7 @@ func LayoutSearch(layout Layout, collection collection.Collection, scene *render sceneMargin := 10. falloff := 5. - scene.Bounds.W = layout.SceneWidth + scene.Bounds.W = layout.ViewportWidth rect := render.Rect{ X: sceneMargin, diff --git a/internal/layout/strip.go b/internal/layout/strip.go new file mode 100644 index 0000000..47478b0 --- /dev/null +++ b/internal/layout/strip.go @@ -0,0 +1,96 @@ +package layout + +import ( + // . "photofield/internal" + + "log" + "photofield/internal/collection" + "photofield/internal/image" + "photofield/internal/metrics" + "photofield/internal/render" + + "time" +) + +func LayoutStrip(layout Layout, collection collection.Collection, scene *render.Scene, source *image.Source) { + + limit := collection.Limit + + var infos <-chan image.SourcedInfo + + if scene.Search != "" { + infos = image.SimilarityInfosToSourcedInfos( + collection.GetSimilar(source, scene.SearchEmbedding, image.ListOptions{ + Limit: limit, + }), + ) + } else { + infos = collection.GetInfos(source, image.ListOptions{ + OrderBy: image.DateAsc, + Limit: limit, + }) + } + + layout.ImageSpacing = 0.02 * layout.ViewportWidth + + rect := render.Rect{ + X: 0, + Y: 0, + W: layout.ViewportWidth, + H: layout.ViewportHeight, + } + + scene.Bounds.H = float64(rect.H) + + scene.Solids = make([]render.Solid, 0) + scene.Texts = make([]render.Text, 0) + + layoutPlaced := metrics.Elapsed("layout placing") + layoutCounter := metrics.Counter{ + Name: "layout", + Interval: 1 * time.Second, + } + + lastLogTime := time.Now() + + scene.Photos = scene.Photos[:0] + index := 0 + for info := range infos { + if limit > 0 && index >= limit { + break + } + + imageRect := render.Rect{ + X: 0, + Y: 0, + W: float64(info.Width), + H: float64(info.Height), + } + + scene.Photos = append(scene.Photos, render.Photo{ + Id: info.Id, + Sprite: render.Sprite{ + Rect: imageRect.FitInside(rect), + }, + }) + + rect.X += float64(rect.W) + layout.ImageSpacing + + now := time.Now() + if now.Sub(lastLogTime) > 1*time.Second { + lastLogTime = now + log.Printf("layout strip %d\n", index) + } + + layoutCounter.Set(index) + index++ + scene.FileCount = index + } + layoutPlaced() + + scene.Bounds.W = rect.X + + scene.RegionSource = PhotoRegionSource{ + Source: source, + } +} diff --git a/internal/layout/timeline.go b/internal/layout/timeline.go index 4196295..c15340e 100644 --- a/internal/layout/timeline.go +++ b/internal/layout/timeline.go @@ -81,7 +81,7 @@ func LayoutTimeline(layout Layout, collection collection.Collection, scene *rend sceneMargin := 10. - scene.Bounds.W = layout.SceneWidth + scene.Bounds.W = layout.ViewportWidth event := TimelineEvent{} eventCount := 0 diff --git a/internal/layout/wall.go b/internal/layout/wall.go index b62fdd6..8f86223 100644 --- a/internal/layout/wall.go +++ b/internal/layout/wall.go @@ -35,11 +35,11 @@ func LayoutWall(layout Layout, collection collection.Collection, scene *render.S edgeCount := int(math.Sqrt(float64(photoCount))) - scene.Bounds.W = layout.SceneWidth + scene.Bounds.W = layout.ViewportWidth cols := edgeCount layoutConfig := Layout{} - layoutConfig.ImageSpacing = layout.SceneWidth / float64(edgeCount) * 0.02 + layoutConfig.ImageSpacing = layout.ViewportWidth / float64(edgeCount) * 0.02 layoutConfig.LineSpacing = layoutConfig.ImageSpacing log.Printf("layout wall width %v cols %v\n", scene.Bounds.W, cols) diff --git a/internal/openapi/api.gen.go b/internal/openapi/api.gen.go index 3ea0c56..3a5d1f6 100644 --- a/internal/openapi/api.gen.go +++ b/internal/openapi/api.gen.go @@ -18,6 +18,8 @@ const ( LayoutTypeSQUARE LayoutType = "SQUARE" + LayoutTypeSTRIP LayoutType = "STRIP" + LayoutTypeTIMELINE LayoutType = "TIMELINE" LayoutTypeWALL LayoutType = "WALL" @@ -118,16 +120,14 @@ type SceneId string // SceneParams defines model for SceneParams. type SceneParams struct { - CollectionId CollectionId `json:"collection_id"` - ImageHeight ImageHeight `json:"image_height"` - Layout LayoutType `json:"layout"` - SceneWidth SceneWidth `json:"scene_width"` - Search *Search `json:"search,omitempty"` + CollectionId CollectionId `json:"collection_id"` + ImageHeight *ImageHeight `json:"image_height,omitempty"` + Layout LayoutType `json:"layout"` + Search *Search `json:"search,omitempty"` + ViewportHeight ViewportHeight `json:"viewport_height"` + ViewportWidth ViewportWidth `json:"viewport_width"` } -// SceneWidth defines model for SceneWidth. -type SceneWidth float32 - // Search defines model for Search. type Search string @@ -154,6 +154,12 @@ type TaskType string // TileCoord defines model for TileCoord. type TileCoord int +// ViewportHeight defines model for ViewportHeight. +type ViewportHeight float32 + +// ViewportWidth defines model for ViewportWidth. +type ViewportWidth float32 + // FileIdPathParam defines model for FileIdPathParam. type FileIdPathParam FileId @@ -166,11 +172,12 @@ type SizePathParam string // GetScenesParams defines parameters for GetScenes. type GetScenesParams struct { // Collection ID - CollectionId CollectionId `json:"collection_id"` - SceneWidth *SceneWidth `json:"scene_width,omitempty"` - ImageHeight *ImageHeight `json:"image_height,omitempty"` - Layout *LayoutType `json:"layout,omitempty"` - Search *Search `json:"search,omitempty"` + CollectionId CollectionId `json:"collection_id"` + ViewportWidth *ViewportWidth `json:"viewport_width,omitempty"` + ViewportHeight *ViewportHeight `json:"viewport_height,omitempty"` + ImageHeight *ImageHeight `json:"image_height,omitempty"` + Layout *LayoutType `json:"layout,omitempty"` + Search *Search `json:"search,omitempty"` } // PostScenesJSONBody defines parameters for PostScenes. @@ -193,6 +200,7 @@ type GetScenesSceneIdRegionsParams struct { // GetScenesSceneIdTilesParams defines parameters for GetScenesSceneIdTiles. type GetScenesSceneIdTilesParams struct { TileSize int `json:"tile_size"` + BackgroundColor *string `json:"background_color,omitempty"` Zoom int `json:"zoom"` X TileCoord `json:"x"` Y TileCoord `json:"y"` @@ -462,14 +470,25 @@ func (siw *ServerInterfaceWrapper) GetScenes(w http.ResponseWriter, r *http.Requ return } - // ------------- Optional query parameter "scene_width" ------------- - if paramValue := r.URL.Query().Get("scene_width"); paramValue != "" { + // ------------- Optional query parameter "viewport_width" ------------- + if paramValue := r.URL.Query().Get("viewport_width"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "viewport_width", r.URL.Query(), ¶ms.ViewportWidth) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter viewport_width: %s", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "viewport_height" ------------- + if paramValue := r.URL.Query().Get("viewport_height"); paramValue != "" { } - err = runtime.BindQueryParameter("form", true, false, "scene_width", r.URL.Query(), ¶ms.SceneWidth) + err = runtime.BindQueryParameter("form", true, false, "viewport_height", r.URL.Query(), ¶ms.ViewportHeight) if err != nil { - http.Error(w, fmt.Sprintf("Invalid format for parameter scene_width: %s", err), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Invalid format for parameter viewport_height: %s", err), http.StatusBadRequest) return } @@ -764,6 +783,17 @@ func (siw *ServerInterfaceWrapper) GetScenesSceneIdTiles(w http.ResponseWriter, return } + // ------------- Optional query parameter "background_color" ------------- + if paramValue := r.URL.Query().Get("background_color"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "background_color", r.URL.Query(), ¶ms.BackgroundColor) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter background_color: %s", err), http.StatusBadRequest) + return + } + // ------------- Required query parameter "zoom" ------------- if paramValue := r.URL.Query().Get("zoom"); paramValue != "" { diff --git a/internal/render/rect.go b/internal/render/rect.go index 5710076..037fd2b 100644 --- a/internal/render/rect.go +++ b/internal/render/rect.go @@ -53,7 +53,7 @@ func (rect Rect) String() string { return fmt.Sprintf("%3.3f %3.3f %3.3f %3.3f", rect.X, rect.Y, rect.W, rect.H) } -func (rect Rect) FitInside(container Rect) Rect { +func (rect Rect) FitInside(container Rect) (out Rect) { imageRatio := rect.W / rect.H var scale float64 @@ -63,12 +63,11 @@ func (rect Rect) FitInside(container Rect) Rect { scale = container.H / rect.H } - return Rect{ - X: container.X, - Y: container.Y, - W: rect.W * scale, - H: rect.H * scale, - } + out.W = rect.W * scale + out.H = rect.H * scale + out.X = container.X + (container.W-out.W)*0.5 + out.Y = container.Y + (container.H-out.H)*0.5 + return out } func (rect Rect) GetMatrix() canvas.Matrix { diff --git a/internal/render/scene.go b/internal/render/scene.go index 4467785..ef35847 100644 --- a/internal/render/scene.go +++ b/internal/render/scene.go @@ -1,6 +1,7 @@ package render import ( + "image/color" "math" "sync" "time" @@ -13,8 +14,9 @@ import ( ) type Render struct { - TileSize int `json:"tile_size"` - MaxSolidPixelArea float64 `json:"max_solid_pixel_area"` + TileSize int `json:"tile_size"` + MaxSolidPixelArea float64 `json:"max_solid_pixel_area"` + BackgroundColor color.Color `json:"background_color"` LogDraws bool DebugOverdraw bool DebugThumbnails bool diff --git a/internal/scene/sceneSource.go b/internal/scene/sceneSource.go index 93b6ad3..948123e 100644 --- a/internal/scene/sceneSource.go +++ b/internal/scene/sceneSource.go @@ -104,6 +104,9 @@ func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Sour case layout.Wall: layout.LayoutWall(config.Layout, config.Collection, &scene, imageSource) + case layout.Strip: + layout.LayoutStrip(config.Layout, config.Collection, &scene, imageSource) + case layout.Search: layout.LayoutSearch(config.Layout, config.Collection, &scene, imageSource) @@ -190,9 +193,15 @@ func sceneConfigEqual(a SceneConfig, b SceneConfig) bool { } } - if a.Layout.SceneWidth != 0 && - b.Layout.SceneWidth != 0 && - a.Layout.SceneWidth != b.Layout.SceneWidth { + if a.Layout.ViewportWidth != 0 && + b.Layout.ViewportWidth != 0 && + a.Layout.ViewportWidth != b.Layout.ViewportWidth { + return false + } + + if a.Layout.ViewportHeight != 0 && + b.Layout.ViewportHeight != 0 && + a.Layout.ViewportHeight != b.Layout.ViewportHeight { return false } diff --git a/main.go b/main.go index e9a52ef..c6156fd 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,11 @@ package main import ( "embed" "encoding/binary" + "encoding/hex" "flag" "fmt" goimage "image" + "image/color" "image/draw" "image/png" "io/fs" @@ -187,7 +189,7 @@ func drawTile(c *canvas.Context, r *render.Render, scene *render.Scene, zoom int c.ResetView() img := r.CanvasImage - draw.Draw(img, img.Bounds(), &goimage.Uniform{canvas.White}, goimage.Point{}, draw.Src) + draw.Draw(img, img.Bounds(), &goimage.Uniform{r.BackgroundColor}, goimage.Point{}, draw.Src) matrix := canvas.Identity. Translate(float64(-tx), float64(-ty+tileSize*float64(zoomPower))). @@ -198,7 +200,6 @@ func drawTile(c *canvas.Context, r *render.Render, scene *render.Scene, zoom int c.SetFillColor(canvas.Black) scene.Draw(r, c, scales, imageSource) - } func getTilePool(config *render.Render) *sync.Pool { @@ -330,14 +331,20 @@ func (*Api) PostScenes(w http.ResponseWriter, r *http.Request) { } sceneConfig := defaultSceneConfig - sceneConfig.Layout.SceneWidth = float64(data.SceneWidth) - sceneConfig.Layout.ImageHeight = float64(data.ImageHeight) + sceneConfig.Layout.ViewportWidth = float64(data.ViewportWidth) + sceneConfig.Layout.ViewportHeight = float64(data.ViewportHeight) + sceneConfig.Layout.ImageHeight = 0 + if data.ImageHeight != nil { + sceneConfig.Layout.ImageHeight = float64(*data.ImageHeight) + } if data.Layout != "" { sceneConfig.Layout.Type = layout.Type(data.Layout) } if data.Search != nil { sceneConfig.Scene.Search = string(*data.Search) - sceneConfig.Layout.Type = layout.Search + if sceneConfig.Layout.Type != layout.Strip { + sceneConfig.Layout.Type = layout.Search + } } collection := getCollectionById(string(data.CollectionId)) if collection == nil { @@ -354,8 +361,11 @@ func (*Api) PostScenes(w http.ResponseWriter, r *http.Request) { func (*Api) GetScenes(w http.ResponseWriter, r *http.Request, params openapi.GetScenesParams) { sceneConfig := defaultSceneConfig - if params.SceneWidth != nil { - sceneConfig.Layout.SceneWidth = float64(*params.SceneWidth) + if params.ViewportWidth != nil { + sceneConfig.Layout.ViewportWidth = float64(*params.ViewportWidth) + } + if params.ViewportHeight != nil { + sceneConfig.Layout.ViewportHeight = float64(*params.ViewportHeight) } if params.ImageHeight != nil { sceneConfig.Layout.ImageHeight = float64(*params.ImageHeight) @@ -365,7 +375,9 @@ func (*Api) GetScenes(w http.ResponseWriter, r *http.Request, params openapi.Get } if params.Search != nil { sceneConfig.Scene.Search = string(*params.Search) - sceneConfig.Layout.Type = layout.Search + if sceneConfig.Layout.Type != layout.Strip { + sceneConfig.Layout.Type = layout.Search + } } collection := getCollectionById(string(params.CollectionId)) if collection == nil { @@ -652,6 +664,20 @@ func GetScenesSceneIdTilesImpl(w http.ResponseWriter, r *http.Request, sceneId o zoom := params.Zoom x := int(params.X) y := int(params.Y) + render.BackgroundColor = color.White + if params.BackgroundColor != nil { + c, err := hex.DecodeString(strings.TrimPrefix(*params.BackgroundColor, "#")) + if err != nil { + problem(w, r, http.StatusBadRequest, "Invalid background color") + return + } + render.BackgroundColor = color.RGBA{ + A: 0xFF, + R: c[0], + G: c[1], + B: c[2], + } + } img, context := getTileImage(&render) defer putTileImage(&render, img) @@ -908,8 +934,9 @@ func loadConfiguration(path string) AppConfig { func addExampleScene() { sceneConfig := defaultSceneConfig sceneConfig.Scene.Id = "Tqcqtc6h69" - sceneConfig.Layout.SceneWidth = 800 - sceneConfig.Layout.ImageHeight = 200 + sceneConfig.Layout.ViewportWidth = 1920 + sceneConfig.Layout.ViewportHeight = 1080 + sceneConfig.Layout.ImageHeight = 300 sceneConfig.Collection = *getCollectionById("vacation-photos") sceneSource.Add(sceneConfig, imageSource) } diff --git a/ui/jsconfig.json b/ui/jsconfig.json new file mode 100644 index 0000000..3631e03 --- /dev/null +++ b/ui/jsconfig.json @@ -0,0 +1,8 @@ +{ + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "jsx": "preserve" + } +} \ No newline at end of file diff --git a/ui/src/App.vue b/ui/src/App.vue index 55d1e88..a4f79de 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -37,15 +37,13 @@ :collections="collections" :collection="collection" :tasks="tasks" - :scene="scene" + :scenes="scenes" tabindex="0" @focusin="collectionExpanded = true" @focusout="collectionExpandedPending = false; collectionExpanded = false" @close="collectionExpanded = false" @reindex="reindex" @reload="reload" - @recreate-scene="recreateScene" - @simulate="simulate" > @@ -98,11 +96,10 @@ collectionId.value && fetchedCollection.value); + const recreateEvent = useEventBus("recreate-scene"); + return { goHome, query, @@ -218,6 +216,7 @@ export default { collection, collections, capabilities, + recreateEvent, } }, async mounted() { @@ -271,7 +270,7 @@ export default { this.collectionExpandedPending = false; }, recreateScene() { - this.$bus.emit("recreate-scene"); + this.recreateEvent.emit(); }, async reindex() { await createTask("INDEX", this.collection?.id); @@ -301,16 +300,6 @@ export default { visibility: immersive ? "hidden" : "auto", }, }) - }, - async simulate() { - this.drawer = false; - this.simulating = true; - const done = () => { - this.simulating = false; - this.$bus.off("simulate-done", done); - } - this.$bus.on("simulate-done", done); - this.$bus.emit("simulate-run"); } } } @@ -406,6 +395,11 @@ export default { background-color: white; --mdc-theme-on-primary: rgba(0,0,0,.87); vertical-align: baseline; + transition: transform 0.2s; +} + +.top-bar.immersive { + transform: translateY(-80px); } .top-bar :deep(.mdc-top-app-bar__title) { @@ -432,9 +426,6 @@ button { --mdc-theme-primary: black; } -.top-bar.immersive { - transform: translateY(-80px); -} .title { cursor: pointer; @@ -490,9 +481,4 @@ button { height: calc(100vh - 64px); } -.viewer.simulating { - width: 1280px; - height: 720px; -} - \ No newline at end of file diff --git a/ui/src/api.js b/ui/src/api.js index e4d521e..f5ed1df 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -1,6 +1,7 @@ import useSWRV from "swrv"; import { computed, watch, ref } from "vue"; import qs from "qs"; +import { useRetry } from "./use"; const host = import.meta.env.VITE_API_HOST || "/api"; @@ -49,8 +50,30 @@ export async function getRegions(sceneId, x, y, w, h) { return response.items; } -export async function getRegion(id, sceneParams) { - return get(`/regions/${id}?${sceneParams}`); +export async function getRegion(sceneId, id) { + return get(`/scenes/${sceneId}/regions/${id}`); +} + +export async function getCenterRegion(sceneId, x, y, w, h) { + const regions = await getRegions(sceneId, x, y, w, h); + if (!regions) return null; + const cx = x + w*0.5; + const cy = y + h*0.5; + let minDistSq = Infinity; + let minRegion = null; + for (let i = 0; i < regions.length; i++) { + const region = regions[i]; + const rcx = region.bounds.x + region.bounds.w*0.5; + const rcy = region.bounds.y + region.bounds.h*0.5; + const dx = rcx - cx; + const dy = rcy - cy; + const distSq = dx*dx + dy*dy; + if (distSq < minDistSq) { + minDistSq = distSq; + minRegion = region; + } + } + return minRegion; } export async function getCollections() { @@ -68,10 +91,11 @@ export async function createTask(type, id) { }); } -export function getTileUrl(sceneId, level, x, y, tileSize, extraParams) { +export function getTileUrl(sceneId, level, x, y, tileSize, backgroundColor, extraParams) { const params = { tile_size: tileSize, zoom: level, + background_color: backgroundColor, x, y, ...extraParams, @@ -115,6 +139,80 @@ export function useApi(getUrl, config) { } } +export function useScene({ + collectionId, + layout, + imageHeight, + viewport, + search, +}) { + + const sceneParams = computed(() => + viewport?.width?.value && + viewport?.height?.value && + { + layout: layout.value, + image_height: imageHeight?.value || undefined, + collection_id: collectionId.value, + viewport_width: viewport.width.value, + viewport_height: viewport.height.value, + search: search?.value || undefined, + } + ); + + const { + items: scenes, + isValidating: scenesLoading, + itemsMutate: scenesMutate, + } = useApi(() => sceneParams.value && `/scenes?` + qs.stringify(sceneParams.value)); + + const scene = computed(() => { + const list = scenes?.value; + if (!list || list.length == 0) return null; + return list[0]; + }); + + const recreateScenesInProgress = ref(0); + const recreateScene = async () => { + recreateScenesInProgress.value = recreateScenesInProgress.value + 1; + const params = sceneParams.value; + await scenesMutate(async () => ([await createScene(params)])); + recreateScenesInProgress.value = recreateScenesInProgress.value - 1; + } + + watch(scenes, async newScene => { + // Create scene if a matching one hasn't been found + if (newScene?.length === 0) { + console.log("scene not found, creating..."); + await recreateScene(); + } + }) + + const { run, reset } = useRetry(scenesMutate); + + const filesPerSecond = ref(0); + watch(scene, async (newValue, oldValue) => { + if (newValue?.loading) { + let prev = oldValue?.file_count || 0; + if (prev > newValue.file_count) { + prev = 0; + } + filesPerSecond.value = newValue.file_count - prev; + run(); + } else { + reset(); + filesPerSecond.value = 0; + } + }) + + return { + scene, + recreate: recreateScene, + loading: scenesLoading, + filesPerSecond, + } +} + async function bufferFetcher(endpoint) { const response = await fetch(host + endpoint); if (!response.ok) { @@ -165,3 +263,4 @@ export function useTasks() { export async function createScene(params) { return await post(`/scenes`, params); } + diff --git a/ui/src/components/CollectionDebug.vue b/ui/src/components/CollectionDebug.vue new file mode 100644 index 0000000..e54b018 --- /dev/null +++ b/ui/src/components/CollectionDebug.vue @@ -0,0 +1,104 @@ + + + + + \ No newline at end of file diff --git a/ui/src/components/CollectionPanel.vue b/ui/src/components/CollectionPanel.vue index d9d18cc..cc59f66 100644 --- a/ui/src/components/CollectionPanel.vue +++ b/ui/src/components/CollectionPanel.vue @@ -2,75 +2,81 @@
- + - - - - +
+ - - {{ c.name }} - - - - + + + + + + {{ c.name }} + + + +
+
\ No newline at end of file diff --git a/ui/src/components/CollectionSettings.vue b/ui/src/components/CollectionSettings.vue index 1429fc7..dcf9cdb 100644 --- a/ui/src/components/CollectionSettings.vue +++ b/ui/src/components/CollectionSettings.vue @@ -1,54 +1,37 @@ \ No newline at end of file diff --git a/ui/src/components/CollectionView.vue b/ui/src/components/CollectionView.vue new file mode 100644 index 0000000..7b257a1 --- /dev/null +++ b/ui/src/components/CollectionView.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/ui/src/components/Controls.vue b/ui/src/components/Controls.vue new file mode 100644 index 0000000..e6fa362 --- /dev/null +++ b/ui/src/components/Controls.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/ui/src/components/NaturalViewer.vue b/ui/src/components/NaturalViewer.vue deleted file mode 100644 index 533d7dc..0000000 --- a/ui/src/components/NaturalViewer.vue +++ /dev/null @@ -1,1174 +0,0 @@ - - - - - diff --git a/ui/src/components/PageTitle.vue b/ui/src/components/PageTitle.vue index c390f6f..6abc8b4 100644 --- a/ui/src/components/PageTitle.vue +++ b/ui/src/components/PageTitle.vue @@ -4,8 +4,9 @@ export default { watch: { title: { immediate: true, - handler() { - document.title = this.title; + handler(title) { + if (!title) return; + document.title = title; }, }, }, diff --git a/ui/src/components/PixelCount.vue b/ui/src/components/PixelCount.vue new file mode 100644 index 0000000..d58195b --- /dev/null +++ b/ui/src/components/PixelCount.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/ui/src/components/RegionMenu.vue b/ui/src/components/RegionMenu.vue index 0b012ba..0c37f53 100644 --- a/ui/src/components/RegionMenu.vue +++ b/ui/src/components/RegionMenu.vue @@ -9,11 +9,10 @@ @@ -66,7 +65,9 @@ import copyImg from 'copy-image-clipboard'; import TileViewer from './TileViewer.vue'; import ExpandButton from './ExpandButton.vue'; -import { getFileBlob, getFileUrl, getThumbnailUrl } from '../api'; +import { getFileUrl, getThumbnailUrl } from '../api'; +import { ref } from 'vue'; +import { useViewport } from '../use'; export default { props: ["region", "scene", "flipX", "flipY", "tileSize"], @@ -78,6 +79,14 @@ export default { expanded: false, } }, + setup() { + const viewer = ref(null); + const viewport = useViewport(viewer); + return { + viewer, + viewport, + } + }, computed: { fileUrl() { const data = this.region?.data; diff --git a/ui/src/components/ScrollViewer.vue b/ui/src/components/ScrollViewer.vue new file mode 100644 index 0000000..1c69bd4 --- /dev/null +++ b/ui/src/components/ScrollViewer.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/ui/src/components/StripViewer.vue b/ui/src/components/StripViewer.vue new file mode 100644 index 0000000..0e7c8a8 --- /dev/null +++ b/ui/src/components/StripViewer.vue @@ -0,0 +1,416 @@ + + + + + diff --git a/ui/src/components/TileViewer.vue b/ui/src/components/TileViewer.vue index f9ffc6e..54e2fe5 100644 --- a/ui/src/components/TileViewer.vue +++ b/ui/src/components/TileViewer.vue @@ -1,5 +1,10 @@