diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..dd8fe2a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,619 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project theoretically adheres to [Semantic
+Versioning](https://semver.org/spec/v2.0.0.html), but it's still at major
+version zero.
+
+## [Unreleased]
+
+### Added
+
+- Assume a timezone based on difference between stored EXIF times
+- Auto-crop black bars from pillarboxed EXIF-embedded thumbnails
+- Allow collections that are defined lower in the configuration with the same
+ name to override the earlier ones
+- Add `created`, `t`, and `dedup` filters (check docs for more)
+- Added "tweaks" for preview features
+
+### Changed
+
+- Fixed excessive CPU usage and glitches due to file watches watching the database
+- Fixed slow home page loading due to unnecessary database queries
+- Fixed image cache size configuration not being applied
+- Increase the default image memory cache size to 1 GiB
+- Fixed default photo width and height across all layouts
+
+### Removed
+
+- TBD
+
+## [v0.15.1] - 2024-05-28 - Timezone & build fixes, and experimental layouts
+
+### Added
+
+* New **Flex layout** using a variant of Knuth & Plass, which makes a smarter layout, especially for odd aspect ratios
+* New experimental **Highlights layout** with a twist! It varies the layout row height based on the "sameness" of the photos. The idea is to make travel photo collections more skimmable by shrinking similar and repeating photos. Requires AI to be enabled.
+
+### Changed
+
+* Upgrade pyroscope-go for Go 1.22
+* Fix `GPSDateTime` handling by not letting it override already-detected timezone-aware dates
+
+[@Terrance](https://github.com/Terrance) made their first contribution in https://github.com/SmilyOrg/photofield/pull/101 🥳
+
+
+
+## [v0.15.0] - 2024-02-18 - Polished interaction with more zoomy bits
+
+### Changed
+- Left/right/down interaction now works a little more smoothly similar to mobile galleries
+- Clicking on a photo now zooms into it directly
+- Lots of other tweaks
+
+### Fixed
+- Video is finally controllable
+- Back button should now work a little more like expected, esp. on mobile
+- Fix dates not showing while scrolling on mobile
+- Selection works on map view now (but it's still useless)
+- A bunch of map fixes
+
+
+
+## [v0.14.2] - 2024-01-07
+
+### Added
+
+Added `PHOTOFIELD_DOCS_PATH` for rewriting paths in documentation so that the same deployment can host the docs at https://photofield.dev and the demo at https://demo.photofield.dev/
+
+### Fixed
+
+Fixed scene constantly loading while contents are being indexed.
+
+
+
+## [v0.14.1] - 2024-01-06 - ARM Docker images
+
+Docker images are now built as multiarch - x64 and arm.
+
+Good for cheaper cloud servers, maybe also M1/M2/M3 Macs (let me know!)
+
+
+
+## [v0.14.0] - 2024-01-06 - Logs, autoreload, errors, fixes
+
+Various quality of life improvements and bug fixes.
+
+- **Logs**: timestamp is not shown anymore as I didn't find it useful (logging systems usually provide their own). Let me know if it was useful to you. All exposed urls are now shown on startup, so it's more obvious how to access it.
+ ```
+ app running (api under /api)
+ local http://127.0.0.1:8080
+ network http://172.22.0.27:8080
+ ```
+- **Loading & errors**: there are more loading indicators, especially for loading collections, so that it doesn't just show a white screen. If the connection to the server drops, there is also a more obvious error message and status shown. Same if you run it without any collections configured. Fixes #84.
+
+- **Autoreload**: the configuration should be automatically reloaded on yaml file change, so restarting is not required anymore. The same applies to collections with `expand_subdirs: true`, if you change the dirs the server is automatically reloaded.
+
+- **Rescan applies faster**: previously the view was sometimes cached "too much" leading to it showing the same thing, even after a rescan and page reload. Switching back to the photos or refreshing the page should now always show the up to date view, resolving point 1 in #81 from the scanning point of view.
+
+- **Cleaner exit**: if you Ctrl+C or otherwise "soft close" (`SIGTERM`) the app, it should close the database cleanly, so `-shm` and `-wal` files are not left laying around anymore. They are still left if you forcibly close the app (`SIGKILL`) as that cannot be handled.
+
+- **Layout fixes**: timeline view shouldn't overlap with the app bar on top anymore. Small collections should now be top-left aligned and not centered. The "Wall" layout was bugged and should now work as expected again.
+
+- **Add Playwright e2e tests**: this should make it easier to make sure that some of the features above keep working as expected in the future. Still a bit of a proof of concept though.
+
+- **File reorganization**: the UI, docs, and e2e tests are now there as three independent nodejs projects so that you don't have to install all of them to work on just one.
+
+
+
+## [v0.13.0] - 2023-11-06 - Embedded docs MVP
+
+It's basically the same as the README right now, but more room for growth.
+
+It's accessible by clicking on the question mark in the top right corner of the app in the collections / home page.
+
+Eventually this should be hosted on https://photofield.dev as the main documentation source with the README being trimmed down to the basics.
+
+
+
+## [v0.12.0] - 2023-10-26 - Map view 🗺
+
+✨ It shows _ALL_ the photos in the collection/album on the map near to where they were taken ✨
+
+### Considerations
+- Works for photos with embedded exif GPS coordinates
+- Since often there are many photos taken in close proximity, there is a balance struck between "not overlapping with other photos", "distance from taken location", and "displayed size".
+- Uses OpenStreetMap for the background map for now, so it's not fully self-hosted.
+ - The photos themselves are rendered locally, but the background map layer is loaded from OSM
+
+It's still somewhat rough, you might need to refresh after it's done loading. If you haven't reindexed metadata since [v0.11.0](https://github.com/SmilyOrg/photofield/releases/tag/v0.11.0), you will need to do it for the GPS coordinates to be picked up
+
+### Changes
+
+* Fixed gradual browser slowdown bugs that have persisted for a while, especially noticeable on low-powered devices.
+
+
+
+## [v0.11.1] - 2023-10-09 - Reverse geolocation by default
+
+Reverse geolocation is now enabled by default using the custom-built [tinygpkg package](https://github.com/SmilyOrg/tinygpkg).
+
+### Changes
+
+- Instant startup compared to ~10s before
+- Increased number of places from 6018 to 49689, powered by https://www.geoboundaries.org/
+- Adds only 16MB to build (uncompressed)
+- Negligible impact on performance
+- Still timeline-only
+
+
+If you don't see any locations, you might need to reindex your metadata.
+
+
+
+## [v0.11.0] - 2023-08-12 - Reverse geolocation
+
+Location names shown in the timeline view via embedded local-only geolocation package rgeo!
+
+Has to be enabled explicitly via `configuration.yaml` due to the relatively long startup time (10s or more) and higher memory usage (~1.5GB), see [defaults.yaml](defaults.yaml#L58-L66) for details.
+
+Thanks to [@zach-capalbo](https://github.com/zach-capalbo) for the contribution and apologies for the delayed release notes!
+
+
+
+## [v0.10.4] - 2023-06-27 - Better loading and blurry photos fix
+
+Tuned image loading to be more efficient and less likely to show blurry photos.
+
+
+
+## [v0.10.3] - 2023-05-18 - Tag search
+
+You can filter photos in the collection by searching for `tag:TAG`.
+
+For example, you can search for `tag:fav` to only show favorited photos, or `tag:hello tag:world` to only show photos with both `hello` and `world` tags. This is an early version of filtering and should be more user-friendly in the future.
+
+Related image search #62, tag filtering, and semantic search #40 currently cannot be combined and only one will be applied (in that order).
+
+
+
+## [v0.10.2] - 2023-05-16 - EXIF tags
+
+### Added
+
+Automatically add tags from EXIF data.
+
+The only EXIF tags are the currently hardcoded `make` and `model`, and they are added to the file as `exif:make:` and `exif:model:` tags respectively.
+
+To enable the automatic addition of these tags, you need to enable it in the config.
+```yaml
+tags:
+ enable: true
+ exif:
+ enable: true
+```
+
+### Fixed
+
+Fix `tags.enable` to now actually work, as only `enabled` worked before. `enabled` will keep working for now to not break existing configurations.
+
+
+
+## [v0.10.1] - 2023-05-14 - Related image search
+
+* Adds **Find Similar Images** to the context menu
+* Adds a basic search query parser currently supporting `img:ID` for searching related images
+* This can be extended later filtering for tags, arithmetic/boolean logic, etc.
+
+
+
+## [v0.10.0] - 2023-05-07 - Tags
+
+In their current form the tags can be a bit volatile, so consider them alpha-level and don't get too attached.
+
+_**Note:** You need to enable tagging explicitly first in the `tags` section of the configuration._
+
+They have some cool features in their current form. The intention here is to form a foundation on top of which many other features can be built. See [H_Q_'s comment thread from a while ago](https://old.reddit.com/r/selfhosted/comments/x601ql/photofield_v05_released_google_photos_alternative/incw9bp/) for details and ideas.
+
+1. **Selection.** You can select photos now via Ctrl + Click or Ctrl + (Shift) + Drag. Selections are handled as "system tags" (tags with `sys:` prefix) and persist across refreshes, restarts, and across browsers (as long as you keep the link and don't delete the database). You cannot do anything else with the selection right now, so functionally they're more of a tech demo (i.e. useless). This will make it easier to implement #4 however.
+
+2. **Tag picker.** You can click the # button to show a photo's tags (excluding "system tags"). You can add and remove tags as you please using the multiselect with auto-complete. The ΓÖÑ button toggles a `fav` tag as a simple "liking" functionality.
+
+3. **Range tree tagging.** This is an interesting implementation detail that makes it so that in some cases tags can be stored in a compressed "id range tree" manner, so that you can theoretically select/tag thousands of photos all at once. This should make it a lot more efficient to also add e.g. location tags to large subsets of photos for example as part of #59
+
+I'm not 100% happy with the way tag IDs/revisions/etc. work right now, so that's something to look at in the future, but it's alright as a first draft.
+
+Clearly the biggest missing part is search and/or filtering, which will actually make them a bit more useful than what is there. But let's see :)
+
+Also bonus: fixes #21 in a hacky way (show the hand cursor for all canvas interaction regardless of photo or background)
+
+
+
+## [v0.9.4] - 2023-04-23
+
+* Add FFmpeg hack to make RAW files brighter
+
+
+
+## [v0.9.3] - 2023-04-23
+
+* Dependency updates
+* Fix `panic: determinant of affine transformation matrix is zero` for some cases
+
+
+
+## [v0.9.2] - 2023-04-11
+
+* Disable AI indexing if there is no visual host
+* Remove search input on collections view
+* Tuning to pick higher resolution thumbnails more eagerly
+
+
+
+## [v0.9.1] - 2023-04-10
+
+These changes were intended to be part of v0.9.0, but were mistakenly left out.
+
+* Adds pyroscope support for performance profiling the app
+* More flexible image variant source configuration
+* Fix timeline view + upgrade build
+
+
+
+## [v0.9.0] - 2023-04-10 - Refactor image loading and rendering
+
+Replaces previous image and thumbnail loading/rendering system with a more flexible one.
+
+* New system supports all image formats that FFmpeg does, including AVIF and some RAW formats
+* Added sqlite-based thumbnail generation system to resolve the limitation of pre-generated thumbnails
+* Some bugs may still be present as testing was limited
+
+
+
+## [v0.8.0] - 2023-01-08 - Strip Viewer
+
+Clicking now opens the new strip viewer, but you can still zoom in the original layout with ctrl+zoom or with pinch-to-zoom.
+
+As this is a big refactor it likely has some remaining bugs, oddities, and visual quirks.
+
+
+
+## [v0.7.0] - 2022-10-12 - Remove sidebar
+
+The sidebar has been removed in favor of a dropdown menu for collections.
+
+
+
+## [v0.6.2] - 2022-10-11
+
+* Disable loading AI if not available
+* Split AI hosts support
+
+
+
+## [v0.6.1] - 2022-10-09
+
+Upgrade to golang 1.19 in GitHub workflow.
+
+
+
+## [v0.6.0] - 2022-10-09 - Add semantic search using photofield-ai
+
+If you set up a [photofield-ai](https://github.com/SmilyOrg/photofield-ai) server and configure it in the `ai` section of the configuration, you should be able to search for photo contents using words like "beach sunset", "a couple kissing", or "cat eyes".
+
+## [v0.5.3] - 2022-09-18 - Context menu fixes
+
+Fixes a bunch of bugs with the context menu closing at bad times and the focus not working right.
+
+
+
+## [v0.5.2] - 2022-09-18
+
+Better memory cache size estimation, potentially making images load slightly faster.
+
+
+
+## [v0.5.1] - 2022-09-06
+
+Fix some off-by-one navigation issues.
+
+
+
+## [v0.5.0] - 2022-09-04 - OpenLayers
+
+- Replace OpenSeadragon with OpenLayers for tile rendering
+- Fix date and small scene handling
+
+There might be some bugs from the reimplementation, but the rendering seems a lot faster (especially on low power devices) and it doesn't slow down over time as was the case with OpenSeadragon.
+
+
+
+## [v0.4.5] - 2022-08-15
+
+Fix the CORS configuration being too loose by default. This should improve security somewhat.
+
+
+
+## [v0.4.4] - 2022-07-31
+
+Logging error hotfix
+
+
+
+## [v0.4.3] - 2022-07-31
+
+- Reduce memory usage by pruning scenes
+- Add more logs for easier debugging
+
+
+
+## [v0.4.2] - 2022-07-26
+
+Fix dates getting stuck sometimes
+
+
+
+## [v0.4.1] - 2022-07-26 - Preload all scrollbar dates and UI update
+
+The scrollbar dates don't need a server check to load anymore as they are all preloaded on scene load. The UI has been changed so that instead of the dates showing up next to the scrollbar, they are fixed to the top left corner and only show up while scrolling quickly (similar to Google Photos on mobile).
+
+
+
+## [v0.4.0] - 2022-07-16 - Large collections
+
+Loading large collections can be way faster now (loads at 100k-1000k or more photos / second).
+
+Collections of more than 100M photos should also work, but as I don't have that many photos I've only been able to try it by adding a hack to repeat a 40k collection thousands of times.
+
+There is a frontend-based loader now while a scene is loading. This makes it more obvious what's going on without checking the logs.
+PhotofieldMillions2
+
+Image/video thumbnails/variants have been refactored to fix some issues with the previous setup. The configuration has changed slightly to support this. Video thumbnails should also show up faster and throw fewer errors in the logs now.
+
+
+
+## [v0.3.3] - 2022-07-09 - Frontend optimization
+
+The app wasn't scoring that well on Lighthouse due to a variety of small low-hanging fruit optimizations that were missing (like gzipping js and css) and cache policies. This change addresses most of those and updates frontend dependencies while we're at it.
+
+The following examples are for an album with ~5000 photos on a DS418play (warm caches).
+
+![lighthouse score before](https://user-images.githubusercontent.com/1451391/178107242-c1b97a8c-deb0-4536-9777-0b754c14aa76.png)
+
+_Photofield Lighthouse performance score was 76 before the optimizations_
+
+Adding these smaller optimizations makes it much better, even scoring 100 on the performance metric.
+
+![lighthouse score after](https://user-images.githubusercontent.com/1451391/178106178-d9d930c3-3065-496a-9a35-263e2f57362d.png)
+
+_Photofield Lighthouse performance score is 100 after optimizations_
+
+For reference, here is the same album with Google Photos (yes, it's slower!).
+
+![google photos lighthouse score](https://user-images.githubusercontent.com/1451391/178106326-8e5f005d-56c9-45dc-b7cc-380fd3c46d01.png)
+
+_Google Photos Lighthouse performance score_
+
+
+
+## [v0.3.2] - 2022-07-08 - Smoother scrolling on slow devices
+
+Previously the whole canvas was redrawn every time while scrolling. Fast devices like PCs had no problem with this, but slower devices, like phones, tablets and TVs were not able to keep up the redraws while scrolling leading to a laggy experience sometimes nearing slideshow levels. With this release the canvas is scrolled natively by the browser, which can be a lot smoother.
+
+### Technical Details
+
+Since the canvas is virtual, the native scrolling needs to be combined with redrawing so that the photos never "scroll out of view". If the device is slow and is unable to keep up with the redraws, this now presents itself as "white space" where the redraw has not happened yet. This seems like a better tradeoff as it feels a lot better to have a smooth scroll with some edge artifacts than an always-stable, but laggy scrolling.
+
+These artifacts could be overcome by overdrawing at the edges, so that there is some wiggle room before a redraw needs to happen. This is not implemented yet as a larger canvas can also lead to slower redraws in the first place, so a balance would need to be found.
+
+
+
+## [v0.3.1] - 2022-06-16 - Make it Actually Work
+
+### Added
+
+* Environment variable to specify a custom server address by @luusl in https://github.com/SmilyOrg/photofield/pull/7
+
+### Fixed
+
+* App refusing to start if you don't already have a database
+* App showing a blank white screen in browser (in some cases, especially Windows)
+
+
+
+## [v0.3.0] - 2022-06-06 - More Efficient Database
+
+This release should be functionally equivalent to v0.2.1, however it reorganizes database storage and processing so that it both takes less space and runs faster (especially scene creation on slow disks).
+
+As an example, in v0.2.1, it takes up 193MiB for my database of ~540k photos. In v0.3.0 the same database with the same data takes up 74MiB.
+
+### More Detail
+
+Both sizes are after vacuuming for a more accurate comparison. You can run a vacuum as a [maintenance task](https://github.com/SmilyOrg/photofield/#maintenance) through the app itself now too. Ideally in the future this would be automatic, but for now you can run it now and then if you'd like to reclaim some space after adding/removing a lot of files.
+
+### Even More Detail
+
+Previously all the file paths were stored as absolute paths, which worked pretty well, but if you have a lot of files, it can lead to a lot of duplication as the same long path is stored for each file. This release de-duplicates all the directories into a separate table, so each file only references this table. It almost sounds like I'm reimplementing file systems here, but I swear it makes sense
+
+This leads to only the file names and a reference to a directory having to be stored. With the above real-world collection of 540k photos, they are only part of ~4600 directories, leading to the big storage savings. This complicates the queries a bit, but SQLite does a good job at executing them.
+
+Due to internal reworking, file paths are also no longer needed when listing / opening a collection (album), so the query can just filter to a few specific directory ids, returning the file metadata in order. With the right indexes this can be pretty fast and efficient. At least that's the idea
+
+
+
+## [v0.2.1] - 2022-05-21
+
+* Add embedded jpeg docs (forgot)
+* Build on new tags only
+* Sort by UTC time
+
+
+
+## [v0.2.0] - 2021-12-01 - Embedded JPEG thumbnails
+
+Embedded JPEG thumbnails are now supported. Extracting them is slower than loading pregenerated thumbnails already on disk, but way faster than loading the original, so it's a nice middle ground if you don't already have thumbnails.
+![Photofield8](https://user-images.githubusercontent.com/1451391/144293168-173845cf-e0bd-4d95-ab80-ceca55e8b6ef.gif)
+
+### Debug Modes
+
+Additionally, the two debug modes supported by the API are now accessible again. They exist to more easily debug how and when the thumbnails are being used.
+
+#### Debug Overdraw
+
+Shows how close to "perfect" is the thumbnail / image being loaded.
+More red 👉 too high resolution / wasted loaded pixels / slow loading
+More blue 👉 not enough resolution / blurry display
+
+Using only embedded JPEG thumbnails shows that when the original images has to be used, it is way too high of a resolution for the currently displayed size, so it's slow.
+
+https://user-images.githubusercontent.com/1451391/144292927-0e5e3d3b-84a0-4d36-bb83-7a8a8f7cd8d6.mp4
+
+#### Debug Thumbnails
+
+Shows the resolution of the thumbnail / image being used, the "distance" from the ideal resolution for the currently displayed size and the name of the thumbnail (or `original` for the original photo).
+
+https://user-images.githubusercontent.com/1451391/144292940-f298474f-2924-42b2-83a0-b5646b00d186.mp4
+
+### Changelog
+
+* e95e1c3 Merge pull request #5 from SmilyOrg/embedded
+* 9aa66f8 Support embedded jpeg thumbs + fix debug modes
+
+
+### Docker images
+
+- `docker pull ghcr.io/smilyorg/photofield:v0.2.0`
+- `docker pull ghcr.io/smilyorg/photofield:v0`
+- `docker pull ghcr.io/smilyorg/photofield:v0.2`
+
+
+
+## [v0.1.10] - 2021-11-08
+
+Fixed default Docker data dir `PHOTOFIELD_DATA_DIR` in actual container releases and not just `Dockerfile`.
+
+
+
+## [v0.1.9] - 2021-10-03
+
+GitHub workflow debugging and improved README.
+
+
+
+## [v0.1.8] - 2021-10-02
+
+### Added
+
+- README.md
+- `defaults.yaml` documentation
+
+
+
+## [v0.1.7] - 2021-09-27
+
+GitHub workflow debugging.
+
+
+
+## [v0.1.6] - 2021-09-27
+
+GitHub workflow debugging.
+
+
+
+## [v0.1.5] - 2021-09-27
+
+Fixed `PHOTOFIELD_DATA_DIR` in Docker image to point to the right place.
+
+
+
+## [v0.1.4] - 2021-09-26
+
+GitHub workflow debugging.
+
+### Added
+
+- `jpeg` and `png` to default extensions
+
+
+
+## [v0.1.3] - 2021-09-25
+
+Identical to v0.1.2.
+
+
+
+## [v0.1.2] - 2021-09-25
+
+GitHub workflow debugging.
+
+
+
+## [v0.1.1] - 2021-09-25
+
+GitHub templates and workflow debugging.
+
+
+
+## [v0.1.0] - 2021-09-25
+
+First release
+
+
+
+
+
+[unreleased]: https://github.com/SmilyOrg/photofield/compare/v0.15.1...HEAD
+[v0.15.1]: https://github.com/SmilyOrg/photofield/compare/v0.15.0...v0.15.1
+[v0.15.0]: https://github.com/SmilyOrg/photofield/compare/v0.14.2...v0.15.0
+[v0.14.2]: https://github.com/SmilyOrg/photofield/compare/v0.14.1...v0.14.2
+[v0.14.1]: https://github.com/SmilyOrg/photofield/compare/v0.14.0...v0.14.1
+[v0.14.0]: https://github.com/SmilyOrg/photofield/compare/v0.13.0...v0.14.0
+[v0.13.0]: https://github.com/SmilyOrg/photofield/compare/v0.12.0...v0.13.0
+[v0.12.0]: https://github.com/SmilyOrg/photofield/compare/v0.11.1...v0.12.0
+[v0.11.1]: https://github.com/SmilyOrg/photofield/compare/v0.11.0...v0.11.1
+[v0.11.0]: https://github.com/SmilyOrg/photofield/compare/v0.10.4...v0.11.0
+[v0.10.4]: https://github.com/SmilyOrg/photofield/compare/v0.10.3...v0.10.4
+[v0.10.3]: https://github.com/SmilyOrg/photofield/compare/v0.10.2...v0.10.3
+[v0.10.2]: https://github.com/SmilyOrg/photofield/compare/v0.10.1...v0.10.2
+[v0.10.1]: https://github.com/SmilyOrg/photofield/compare/v0.10.0...v0.10.1
+[v0.10.0]: https://github.com/SmilyOrg/photofield/compare/v0.9.4...v0.10.0
+[v0.9.4]: https://github.com/SmilyOrg/photofield/compare/v0.9.3...v0.9.4
+[v0.9.3]: https://github.com/SmilyOrg/photofield/compare/v0.9.2...v0.9.3
+[v0.9.2]: https://github.com/SmilyOrg/photofield/compare/v0.9.1...v0.9.2
+[v0.9.1]: https://github.com/SmilyOrg/photofield/compare/v0.9.0...v0.9.1
+[v0.9.0]: https://github.com/SmilyOrg/photofield/compare/v0.8.0...v0.9.0
+[v0.8.0]: https://github.com/SmilyOrg/photofield/compare/v0.7.0...v0.8.0
+[v0.7.0]: https://github.com/SmilyOrg/photofield/compare/v0.6.2...v0.7.0
+[v0.6.2]: https://github.com/SmilyOrg/photofield/compare/v0.6.1...v0.6.2
+[v0.6.1]: https://github.com/SmilyOrg/photofield/compare/v0.6.0...v0.6.1
+[v0.6.0]: https://github.com/SmilyOrg/photofield/compare/v0.5.3...v0.6.0
+[v0.5.3]: https://github.com/SmilyOrg/photofield/compare/v0.5.2...v0.5.3
+[v0.5.2]: https://github.com/SmilyOrg/photofield/compare/v0.5.1...v0.5.2
+[v0.5.1]: https://github.com/SmilyOrg/photofield/compare/v0.5.0...v0.5.1
+[v0.5.0]: https://github.com/SmilyOrg/photofield/compare/v0.4.5...v0.5.0
+[v0.4.5]: https://github.com/SmilyOrg/photofield/compare/v0.4.4...v0.4.5
+[v0.4.4]: https://github.com/SmilyOrg/photofield/compare/v0.4.3...v0.4.4
+[v0.4.3]: https://github.com/SmilyOrg/photofield/compare/v0.4.2...v0.4.3
+[v0.4.2]: https://github.com/SmilyOrg/photofield/compare/v0.4.1...v0.4.2
+[v0.4.1]: https://github.com/SmilyOrg/photofield/compare/v0.4.0...v0.4.1
+[v0.4.0]: https://github.com/SmilyOrg/photofield/compare/v0.3.3...v0.4.0
+[v0.3.3]: https://github.com/SmilyOrg/photofield/compare/v0.3.2...v0.3.3
+[v0.3.2]: https://github.com/SmilyOrg/photofield/compare/v0.3.1...v0.3.2
+[v0.3.1]: https://github.com/SmilyOrg/photofield/compare/v0.3.0...v0.3.1
+[v0.3.0]: https://github.com/SmilyOrg/photofield/compare/v0.2.1...v0.3.0
+[v0.2.1]: https://github.com/SmilyOrg/photofield/compare/v0.2.0...v0.2.1
+[v0.2.0]: https://github.com/SmilyOrg/photofield/compare/v0.1.10...v0.2.0
+[v0.1.10]: https://github.com/SmilyOrg/photofield/compare/v0.1.9...v0.1.10
+[v0.1.9]: https://github.com/SmilyOrg/photofield/compare/v0.1.8...v0.1.9
+[v0.1.8]: https://github.com/SmilyOrg/photofield/compare/v0.1.7...v0.1.8
+[v0.1.7]: https://github.com/SmilyOrg/photofield/compare/v0.1.6...v0.1.7
+[v0.1.6]: https://github.com/SmilyOrg/photofield/compare/v0.1.5...v0.1.6
+[v0.1.5]: https://github.com/SmilyOrg/photofield/compare/v0.1.4...v0.1.5
+[v0.1.4]: https://github.com/SmilyOrg/photofield/compare/v0.1.3...v0.1.4
+[v0.1.3]: https://github.com/SmilyOrg/photofield/compare/v0.1.2...v0.1.3
+[v0.1.2]: https://github.com/SmilyOrg/photofield/compare/v0.1.1...v0.1.2
+[v0.1.1]: https://github.com/SmilyOrg/photofield/compare/v0.1.0...v0.1.1
+[v0.1.0]: https://github.com/SmilyOrg/photofield/releases/tag/v0.1.0
diff --git a/README.md b/README.md
index 8bb666a..0f775ab 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,13 @@
Experimental fast photo viewer.
- Demo
- ·
- Report Bug
- ·
- Request Feature
+
+
+
+
+
+
+
@@ -79,61 +81,19 @@ form of feedback to not lose track.
* **Different layouts**. Collections of photos can be displayed with different
layouts.
![layout examples](docs/assets/layouts.png)
- * **Album:** chronological photos grouped by event
- * **Timeline:** reverse-chronological timeline similar to Google Photos
- * **Wall:** a square collage of all the photos, because zooming is fun!
- * **Map:** all the photos on a map? Sure!
- * [More future ideas?](https://github.com/SmilyOrg/photofield/issues/1)
-* **Semantic search using [photofield-ai] (alpha)**. If you set up an AI server
- and configure it in the `ai` section of the [configuration], you should be
- able to search for photo contents using words like "beach sunset", "a couple
- kissing", or "cat eyes".
- ![semantic search for "cat eyes"](docs/assets/semantic-search.jpg)
-* **Tagging (alpha)**. You can tag photos with arbitrary tags. Currently tags
- are only stored in the database and not in the photos themselves. You need to
- enable them in the `tags` section of the [configuration] and restart the
- server. This forms a foundation for many other features, see below (checked -
- implemented).
- * [x] **Persistent photo selection**. You can Ctrl+Click or Ctrl+Drag to
- select photos. This creates a new randomly generated "selection" tag that is
- persistent and shareable. It also means you can select tens of thousands of
- photos without losing your progress. These tags are currently never cleaned
- up and you can't do anything with it yet, so it's not useful yet, but it's
- a start.
- * [x] **Custom tags**. You canadd your own tags to photos, e.g. `#family` or
- `#vacation`. Batch tagging not supported yet, but should be relatively easy
- to add considering the selections (above) are already tags.
- * [x] **EXIF tags**. Tags are automatically added from the EXIF data, e.g.
- `exif:make:sony` or `exif:model:sm-g950f`. You need to enable this in the
- `exif` section of the [configuration]. Only `make` and `model` are currently
- supported (hardcoded).
- * [x] **Filter by tags**. You can filter by a tag by searching for `tag:TAG`.
- For example, you can search for `tag:fav` to only show favorited photos, or
- `tag:hello tag:world` to only show photos with both `hello` and `world`
- tags. This is an early version of filtering and should be more user-friendly
- in the future.
- * [ ] **Location tags**. Photos could be automatically tagged with the
- location, e.g. `city:berlin` or `country:germany`. See #59.
- * [ ] **Face recognition**. Photos could be automatically tagged with the
- person's name. This would be a great way to search for photos of a specific
- person.
- * **Reverse geolocation**. Local, embedded reverse geolocation via [tinygpkg].
- Does not need any API calls, has negligible performance impact, and supports
- ~50 thousand places. Currently only supported for photos with GPS coordinates
- in the EXIF data and the Timeline view.
-
-* **Flexible media/thumbnail system**. Do you have hundreds of gigabytes of existing
- thumbnails from an existing system? Me too! Let's reuse those. Don't have any?
- No worries, they will be generated automatically to speed up display. Here are
- the currently supported thumbnail sources:
- * Bespoke SQLite thumbnail database - `photofield.thumbs.db`.
- * Synology Moments / Photo Station auto-generated thumbnails in `@eaDir`.
- * Embedded JPEG thumbnails - `ThumbnailImage` Exif tag.
- * Native Go [image](https://pkg.go.dev/image) package.
- * FFmpeg on-the-fly conversion - thumbnails and full sized variants.
- * Configurable via the `sources` section of the [Configuration].
- * Please [open an issue] for other systems, bonus points for an idea on how to
- integrate!
+* **Semantic search using [photofield-ai] (alpha)**. If enabled, you can search
+ for photo contents using words like "beach sunset", "a couple kissing", or
+ "cat eyes". ![semantic search for "cat eyes"](docs/assets/semantic-search.jpg)
+* **Tagging (alpha)**. You can tag and search photos with arbitrary tags. If
+ enabled, tags are stored in the cache database and can be used to filter
+ photos.
+* **Reverse geolocation**. Local, embedded reverse geolocation of ~50 thousand
+ places via [tinygpkg] with neglibile overhead supported in the Timeline and
+ Flex layouts.
+* **Flexible media/thumbnail system**. There are many different ways for images
+ and thumbnails to be generated and stored. Uses FFmpeg for on-the-fly
+ conversion, SQLite for caching, existing embedded JPEG thumbnails, Synology
+ Moments / Photo Station thumbnails, and more.
* **Single file binary**. Thanks to [Go] and [GoReleaser], all the dependencies
are packed into a [single binary file](#binaries) for most major OSes.
* **Read-only file system based collections**. Photofield never changes your
@@ -165,6 +125,8 @@ some time with a slow CPU and cold HDD cache.
* **No permalinks**. Deep linking to images works, but it's currently not stable
over time as IDs can change.
+See the [documentation] for more information.
+
### Built With
* [Go] - API and server-side tile rendering
@@ -269,53 +231,6 @@ collections:
- /photo
```
-
-
-## Usage
-
-This section will cover some obvious uses, but also some possibly unintuitive UI
-quirks that exist in the current version.
-
-### App Bar
-![App bar explanation](docs/assets/app-bar.png)
-
-### Photo Viewer
-
-* Click to zoom to a photo
- * `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
-* Press/hold `Arrow Left` or `Arrow Right` to quickly switch between photos
-* 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)
-
- _You can open/copy/copy link the original or access any existing thumbnails
- that already exist for it with the bottom list of thumbnails by pixel width._
-
-
-
-## Maintenance
-
-Over time the cache database can grow in size due to version upgrades and so on.
-To shrink the database to its minimum size, you can _vacuum_ it. Multiple vacuums in a row have no effect as the vacuum itself rewrites the database from
-the ground up.
-
-While the vacuum is in progress, it will take twice the database size and may
-take several minutes if you have lots of photos and a low-power system.
-
-As an example it took around 5 minutes to vacuum a 260 MiB database containing around 500k photos on a DS418play. The size after vacuuming was 61 MiB as all the
-leftover data from database upgrades was cleaned up.
-
-```sh
-# CLI
-./photofield -vacuum
-
-# Docker
-docker exec -it photofield ./photofield -vacuum
-```
-
## Development Setup
### Prerequisites
@@ -390,6 +305,7 @@ Distributed under the MIT License. See `LICENSE` for more information.
* [readme.so](https://readme.so/)
[Configuration]: #configuration
+[documentation]: https://photofield.dev
[open an issue]: https://github.com/SmilyOrg/photofield/issues
[Getting Started]: #getting-started
diff --git a/api.yaml b/api.yaml
index ddb6cba..77f0aee 100644
--- a/api.yaml
+++ b/api.yaml
@@ -111,6 +111,11 @@ paths:
schema:
$ref: "#/components/schemas/Search"
+ - name: tweaks
+ in: query
+ schema:
+ $ref: "#/components/schemas/Tweaks"
+
responses:
"200":
description: List of scenes created for the specified collection
@@ -227,6 +232,12 @@ paths:
type: boolean
example: false
+ - name: quality_preset
+ in: query
+ schema:
+ type: string
+ example: "HIGH"
+
responses:
"200":
description: OK
@@ -734,6 +745,8 @@ components:
$ref: "#/components/schemas/ViewportWidth"
viewport_height:
$ref: "#/components/schemas/ViewportHeight"
+ tweaks:
+ $ref: "#/components/schemas/Tweaks"
image_height:
$ref: "#/components/schemas/ImageHeight"
layout:
@@ -819,6 +832,9 @@ components:
minimum: 0
example: 800
+ Tweaks:
+ type: string
+
ImageWidth:
type: number
minimum: 0
diff --git a/config.go b/config.go
index b5c7a6b..0645e6e 100644
--- a/config.go
+++ b/config.go
@@ -125,26 +125,25 @@ func loadConfig(dataDir string) (*AppConfig, error) {
}
// Expand collections
- expanded := make([]collection.Collection, 0, len(appConfig.Collections))
+ collections := make([]collection.Collection, 0, len(appConfig.Collections))
expandedDirs := make(map[string]bool) // Track deduplicated dirs
for _, collection := range appConfig.Collections {
if collection.ExpandSubdirs {
for _, dir := range collection.Dirs {
expandedDirs[dir] = true
}
- expanded = append(expanded, collection.Expand()...)
+ collections = append(collections, collection.Expand()...)
} else {
- expanded = append(expanded, collection)
+ collections = append(collections, collection)
}
}
- appConfig.Collections = expanded
appConfig.ExpandedPaths = make([]string, 0, len(expandedDirs))
for dir := range expandedDirs {
appConfig.ExpandedPaths = append(appConfig.ExpandedPaths, dir)
}
- for i := range appConfig.Collections {
- collection := &appConfig.Collections[i]
+ for i := range collections {
+ collection := &collections[i]
collection.GenerateId()
collection.Layout = strings.ToUpper(collection.Layout)
if collection.Limit > 0 && collection.IndexLimit == 0 {
@@ -152,6 +151,19 @@ func loadConfig(dataDir string) (*AppConfig, error) {
}
}
+ // Override earlier collection with the same ID
+ collectionsMap := make(map[string]int)
+ for i := 0; i < len(collections); i++ {
+ if idx, exists := collectionsMap[collections[i].Id]; exists {
+ collections[idx] = collections[i]
+ collections = append(collections[:i], collections[i+1:]...)
+ i--
+ continue
+ }
+ collectionsMap[collections[i].Id] = i
+ }
+ appConfig.Collections = collections
+
appConfig.Media.AI = appConfig.AI
appConfig.Media.DataDir = dataDir
appConfig.Tags.Enable = appConfig.Tags.Enable || appConfig.Tags.Enabled
diff --git a/defaults.yaml b/defaults.yaml
index 3096c9f..f3cfc80 100644
--- a/defaults.yaml
+++ b/defaults.yaml
@@ -15,6 +15,10 @@ collections:
# - /second/dir
# - C:/third/windows/dir
# - ./relative/dir
+ #
+ # Later collections override earlier ones with the same name / id
+ # so that you can have expanded collections and override settings
+ # of specific collections.
# Default layout of all collections
layout:
@@ -108,7 +112,7 @@ media:
# Size of the image cache used while rendering images
# A larger cache might make display/rendering faster, while a smaller
# cache will conserve memory.
- max_size: 256Mi
+ max_size: 1024Mi
# File extensions to index on the file system
extensions: [
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index c7cb8d0..f0bc6df 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -30,6 +30,16 @@ export default defineConfig({
{ text: 'Quick Start', link: '/quick-start' },
]
},
+ {
+ text: 'Features',
+ link: '/features',
+ items: [
+ { text: 'Layouts', link: '/features/layouts' },
+ { text: 'Search', link: '/features/search' },
+ { text: 'Tags', link: '/features/tags' },
+ { text: 'Reverse Geolocation', link: '/features/geolocation' },
+ ]
+ },
{
text: 'Usage',
link: '/usage',
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index 69eb8fe..f698808 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -2,6 +2,15 @@
--vp-c-brand-1: #dd8888;
--vp-c-brand-2: #dd8888;
--vp-c-brand-3: #be6868;
+ --vp-code-color: #c46363;
+ --vp-code-bg: hsl(0, 0%, 97%);
+ --vp-custom-block-tip-code-bg: hsl(237, 100%, 99%);
+}
+
+:root.dark {
+ --vp-code-color: var(--vp-c-brand-1);
+ --vp-code-bg: var(--vp-c-default-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-custom-block-tip-code-bg);
}
.VPFeature article > img {
diff --git a/docs/assets/album.jpg b/docs/assets/album.jpg
new file mode 100644
index 0000000..9fd8fae
Binary files /dev/null and b/docs/assets/album.jpg differ
diff --git a/docs/assets/flex.png b/docs/assets/flex.png
new file mode 100644
index 0000000..7048a79
Binary files /dev/null and b/docs/assets/flex.png differ
diff --git a/docs/assets/highlights.png b/docs/assets/highlights.png
new file mode 100644
index 0000000..c279f3d
Binary files /dev/null and b/docs/assets/highlights.png differ
diff --git a/docs/assets/map.jpg b/docs/assets/map.jpg
new file mode 100644
index 0000000..fb91ee0
Binary files /dev/null and b/docs/assets/map.jpg differ
diff --git a/docs/assets/timeline.jpg b/docs/assets/timeline.jpg
new file mode 100644
index 0000000..27d2047
Binary files /dev/null and b/docs/assets/timeline.jpg differ
diff --git a/docs/assets/wall.jpg b/docs/assets/wall.jpg
new file mode 100644
index 0000000..5deddf9
Binary files /dev/null and b/docs/assets/wall.jpg differ
diff --git a/docs/features/geolocation.md b/docs/features/geolocation.md
new file mode 100644
index 0000000..a403d24
--- /dev/null
+++ b/docs/features/geolocation.md
@@ -0,0 +1,14 @@
+# Reverse Geolocation
+
+Local, embedded reverse geolocation using the custom-built [tinygpkg package].
+
+* Supports ~50 thousand places
+* Powered by https://www.geoboundaries.org/
+* Adds only 16MB to build (uncompressed)
+* Only supports photos with GPS coordinates in the EXIF data
+* Supported in the [Timeline] and [Flex] views
+
+[Timeline]: layouts.md#timeline
+[Flex]: layouts.md#flex
+
+[tinygpkg package]: https://github.com/SmilyOrg/tinygpkg
\ No newline at end of file
diff --git a/docs/features/layouts.md b/docs/features/layouts.md
new file mode 100644
index 0000000..9769a67
--- /dev/null
+++ b/docs/features/layouts.md
@@ -0,0 +1,51 @@
+# Layouts
+
+Photofield supports various layouts to display collections of photos. Each
+layout offers a unique way to organize and view your photos.
+
+You can set the default layout for each collection in the
+[configuration](../configuration), or change the currently displayed layout
+through the cog icon in the top right corner.
+
+## Album
+
+The **Album** layout groups photos chronologically by event. This layout is
+ideal for organizing photos from different events or occasions.
+
+![Album screenshot](../assets/album.jpg)
+
+## Timeline
+
+The **Timeline** layout displays photos in a reverse-chronological order,
+similar to Google Photos. This layout is useful for viewing recent photos first.
+
+![Timeline layout example](../assets/timeline.jpg)
+
+## Wall
+
+The **Wall** layout creates a square collage of all the photos. This layout is
+great for quickly browsing through a large number of photos.
+
+![Wall layout example](../assets/wall.jpg)
+
+## Map
+
+The **Map** layout places all the photos on a map. This layout is perfect for
+finding photos taken at specific locations.
+
+![Map layout example](../assets/map.jpg)
+
+## Flex
+
+The **Flex** layout uses a variant of Knuth & Plass algorithm to create a
+smarter layout, especially for photos with odd aspect ratios.
+
+![Flex layout example](../assets/flex.png)
+
+## Highlights
+
+The **Highlights** layout varies the row height based on the "sameness" of the
+photos. This layout is designed to make travel photo collections more skimmable
+by shrinking similar and repeating photos. This layout requires AI to be enabled.
+
+![Highlights layout example](../assets/highlights.png)
diff --git a/docs/features/search.md b/docs/features/search.md
new file mode 100644
index 0000000..3efdb8c
--- /dev/null
+++ b/docs/features/search.md
@@ -0,0 +1,81 @@
+# Search
+
+Photofield offers powerful search capabilities to help you find photos quickly
+and efficiently. The search feature supports various types of queries, including
+tag-based searches, semantic searches, and more.
+
+::: tip
+Search requires [photofield-ai] to be configured and enabled in the `ai`
+section of the [configuration].
+:::
+
+## Semantic Search
+
+Semantic search allows you to search for photo contents using descriptive words
+like "beach sunset", "a couple kissing", or "cat eyes".
+
+![Semantic search example](../assets/semantic-search.jpg)
+
+By default, the results are sorted by the semantic relevance to the query.
+
+To filter the results instead of sorting them, you can use the `t` parameter in
+the query. For example, `beach sunset t:0.25` will retain the original order of
+the photos, but only keep photos very similar to beach sunsets. The value is
+arbitrary, higher values are more strict and lower values are less strict. A
+good range is usually between 0.1 and 0.3.
+
+[photofield-ai]: https://github.com/smilyorg/photofield-ai
+[configuration]: ../configuration
+
+| Query | Description |
+|-------|-------------|
+| `beach sunset` | Sort photos by how much they look like a beach sunset. |
+| `beach sunset t:0.25` | Filter photos to beach sunsets only. |
+| `beach sunset t:0.15` | Same as above, but less strict. |
+| `a couple kissing` | Find photos of couples kissing. |
+| `rain` | Find the rainiest-looking photos. |
+| `lake t:0.23` | Filter to photos containing a lake. |
+| `upside down` | Filter to photos containing a lake. |
+| `weird angle t:0.25` | Photos taken from strange angles only. |
+
+## Tag Search
+
+::: tip
+Enable tags in the [configuration] to be able to add and search for tags.
+:::
+
+You can filter photos in the collection by searching for specific tags. For
+example, you can search for `tag:fav` to only show favorited photos, or
+`tag:hello tag:world` to only show photos with both `hello` and `world` tags.
+
+| Query | Description |
+|-------|-------------|
+| `tag:fav` | Show all favorited photos. |
+| `tag:vacation tag:beach` | Show photos tagged with both `vacation` and `beach`. |
+
+See the [tags documentation](tags.md) for more on tags.
+
+## Date Range
+
+You can search for photos taken within a specific date range using the `created`
+parameter. The date range should be specified in the format
+`YYYY-MM-DD..YYYY-MM-DD`.
+
+| Query | Description |
+|-------|-------------|
+| `created:2023-01-01..2023-12-31` | Find photos taken in the year 2023. |
+
+## Deduplication
+
+You can use the `dedup` parameter to filter out duplicate successive photos. The
+value should be a threshold between 0 and 1 representing the similarity between
+photos. For example, `dedup:0.9` will filter out photos that are 90% similar to
+each other.
+
+
+
+| Query | Description |
+|-------|-------------|
+| `dedup:0.9` | Filter out photos that are 90% similar to each other. |
+| `dedup:0.5` | Filter out photos that are even kind-of similar. |
+| `dedup:0.3` | Only show very different photos. |
diff --git a/docs/features/tags.md b/docs/features/tags.md
new file mode 100644
index 0000000..314f453
--- /dev/null
+++ b/docs/features/tags.md
@@ -0,0 +1,50 @@
+# Tags
+
+You can tag photos with arbitrary tags. There is only basic support for tags
+right now, but they form a foundation for many other features.
+
+::: warning
+Tags are currently in an alpha state and can be volatile. They are
+not yet stored in the photos themselves, only in the "cache" database.
+:::
+
+Tags needs to be enabled in the `tags` section of the [configuration] the server
+needs to be restarted.
+
+```yaml
+tags:
+ enable: true
+```
+
+[configuration]: ../configuration
+
+## Tagging Photos
+
+If tags are enabled, the fullscreen photo view adds a # (hash) button for toggling the tag selection dropdown. It also adds a 🤍 (heart) button that toggles the `fav` tag to serve as simple "liking" functionality.
+
+## Search
+
+You can filter photos in the collection by searching for `tag:TAG`.
+
+For example, you can search for `tag:fav` to only show favorited photos, or
+`tag:hello tag:world` to only show photos with both `hello` and `world` tags.
+This is an early version of filtering and should be more user-friendly in the
+future.
+
+See the [search documentation](search.md) for more on search.
+
+## EXIF
+
+Automatically add tags from EXIF data.
+
+The only EXIF tags are the currently hardcoded `make` and `model`, and they are
+added to the file as `exif:make:` and `exif:model:` tags
+respectively.
+
+To enable the automatic addition of these tags, you need to enable it in the config.
+```yaml
+tags:
+ enable: true
+ exif:
+ enable: true
+```
diff --git a/docs/index.md b/docs/index.md
index fa43b7d..714b1e5 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -75,6 +75,7 @@ features:
src: /assets/features/map.jpg
title: |
Different views
+ link: /features/layouts
details: |
Collections of photos can be displayed with different layouts, like an album, a timeline, or a map.
@@ -82,13 +83,15 @@ features:
src: /assets/features/slovenia.jpg
title: |
Reverse geolocation
+ link: /features/geolocation
details: |
Local, embedded reverse geolocation, negligible performance impact, no API calls needed. Supports ~50 thousand places powered by geoBoundaries.
- icon:
src: /assets/features/cat-eyes.jpg
title: |
- Semantic search (alpha)
+ Semantic search
+ link: /features/search
details: |
You can search for photo contents using words like "beach sunset", "a couple kissing", or "cat eyes". Needs to be configured as it requires running a separate AI server.
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 00a79f9..d27cf74 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
- "vitepress": "^1.0.0-rc.35"
+ "vitepress": "^1.3.3"
}
},
"node_modules/@algolia/autocomplete-core": {
@@ -50,124 +50,255 @@
}
},
"node_modules/@algolia/cache-browser-local-storage": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.0.tgz",
- "integrity": "sha512-uZ1uZMLDZb4qODLfTSNHxSi4fH9RdrQf7DXEzW01dS8XK7QFtFh29N5NGKa9S+Yudf1vUMIF+/RiL4i/J0pWlQ==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.24.0.tgz",
+ "integrity": "sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==",
"dependencies": {
- "@algolia/cache-common": "4.22.0"
+ "@algolia/cache-common": "4.24.0"
}
},
"node_modules/@algolia/cache-common": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.22.0.tgz",
- "integrity": "sha512-TPwUMlIGPN16eW67qamNQUmxNiGHg/WBqWcrOoCddhqNTqGDPVqmgfaM85LPbt24t3r1z0zEz/tdsmuq3Q6oaA=="
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz",
+ "integrity": "sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g=="
},
"node_modules/@algolia/cache-in-memory": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.22.0.tgz",
- "integrity": "sha512-kf4Cio9NpPjzp1+uXQgL4jsMDeck7MP89BYThSvXSjf2A6qV/0KeqQf90TL2ECS02ovLOBXkk98P7qVarM+zGA==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.24.0.tgz",
+ "integrity": "sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==",
"dependencies": {
- "@algolia/cache-common": "4.22.0"
+ "@algolia/cache-common": "4.24.0"
}
},
"node_modules/@algolia/client-account": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.22.0.tgz",
- "integrity": "sha512-Bjb5UXpWmJT+yGWiqAJL0prkENyEZTBzdC+N1vBuHjwIJcjLMjPB6j1hNBRbT12Lmwi55uzqeMIKS69w+0aPzA==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.24.0.tgz",
+ "integrity": "sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==",
"dependencies": {
- "@algolia/client-common": "4.22.0",
- "@algolia/client-search": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/client-common": "4.24.0",
+ "@algolia/client-search": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/client-account/node_modules/@algolia/client-common": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz",
+ "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/client-account/node_modules/@algolia/client-search": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz",
+ "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==",
+ "dependencies": {
+ "@algolia/client-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
}
},
"node_modules/@algolia/client-analytics": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.22.0.tgz",
- "integrity": "sha512-os2K+kHUcwwRa4ArFl5p/3YbF9lN3TLOPkbXXXxOvDpqFh62n9IRZuzfxpHxMPKAQS3Et1s0BkKavnNP02E9Hg==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.24.0.tgz",
+ "integrity": "sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==",
"dependencies": {
- "@algolia/client-common": "4.22.0",
- "@algolia/client-search": "4.22.0",
- "@algolia/requester-common": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/client-common": "4.24.0",
+ "@algolia/client-search": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
}
},
- "node_modules/@algolia/client-common": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.22.0.tgz",
- "integrity": "sha512-BlbkF4qXVWuwTmYxVWvqtatCR3lzXwxx628p1wj1Q7QP2+LsTmGt1DiUYRuy9jG7iMsnlExby6kRMOOlbhv2Ag==",
+ "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz",
+ "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz",
+ "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==",
"dependencies": {
- "@algolia/requester-common": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/client-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/client-common": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.0.0.tgz",
+ "integrity": "sha512-6N5Qygv/Z/B+rPufnPDLNWgsMf1uubMU7iS52xLcQSLiGlTS4f9eLUrmNXSzHccP33uoFi6xN9craN1sZi5MPQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 14.0.0"
}
},
"node_modules/@algolia/client-personalization": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.22.0.tgz",
- "integrity": "sha512-pEOftCxeBdG5pL97WngOBi9w5Vxr5KCV2j2D+xMVZH8MuU/JX7CglDSDDb0ffQWYqcUN+40Ry+xtXEYaGXTGow==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.24.0.tgz",
+ "integrity": "sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==",
"dependencies": {
- "@algolia/client-common": "4.22.0",
- "@algolia/requester-common": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/client-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz",
+ "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
}
},
"node_modules/@algolia/client-search": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.22.0.tgz",
- "integrity": "sha512-bn4qQiIdRPBGCwsNuuqB8rdHhGKKWIij9OqidM1UkQxnSG8yzxHdb7CujM30pvp5EnV7jTqDZRbxacbjYVW20Q==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.0.0.tgz",
+ "integrity": "sha512-QdDYMzoxYZ3axzBy6CHe+M+NlOGvHEFTa2actchGnp25Uu0N6lyVNivT7nph+P1XoxgAD08cWbeJD3wWQXnpng==",
+ "peer": true,
"dependencies": {
- "@algolia/client-common": "4.22.0",
- "@algolia/requester-common": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/client-common": "5.0.0",
+ "@algolia/requester-browser-xhr": "5.0.0",
+ "@algolia/requester-node-http": "5.0.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
}
},
"node_modules/@algolia/logger-common": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.22.0.tgz",
- "integrity": "sha512-HMUQTID0ucxNCXs5d1eBJ5q/HuKg8rFVE/vOiLaM4Abfeq1YnTtGV3+rFEhOPWhRQxNDd+YHa4q864IMc0zHpQ=="
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz",
+ "integrity": "sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA=="
},
"node_modules/@algolia/logger-console": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.22.0.tgz",
- "integrity": "sha512-7JKb6hgcY64H7CRm3u6DRAiiEVXMvCJV5gRE672QFOUgDxo4aiDpfU61g6Uzy8NKjlEzHMmgG4e2fklELmPXhQ==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.24.0.tgz",
+ "integrity": "sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==",
+ "dependencies": {
+ "@algolia/logger-common": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/recommend": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.24.0.tgz",
+ "integrity": "sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==",
+ "dependencies": {
+ "@algolia/cache-browser-local-storage": "4.24.0",
+ "@algolia/cache-common": "4.24.0",
+ "@algolia/cache-in-memory": "4.24.0",
+ "@algolia/client-common": "4.24.0",
+ "@algolia/client-search": "4.24.0",
+ "@algolia/logger-common": "4.24.0",
+ "@algolia/logger-console": "4.24.0",
+ "@algolia/requester-browser-xhr": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/requester-node-http": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/recommend/node_modules/@algolia/client-common": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz",
+ "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/recommend/node_modules/@algolia/client-search": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz",
+ "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==",
+ "dependencies": {
+ "@algolia/client-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/recommend/node_modules/@algolia/requester-browser-xhr": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz",
+ "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0"
+ }
+ },
+ "node_modules/@algolia/recommend/node_modules/@algolia/requester-node-http": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz",
+ "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==",
"dependencies": {
- "@algolia/logger-common": "4.22.0"
+ "@algolia/requester-common": "4.24.0"
}
},
"node_modules/@algolia/requester-browser-xhr": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.0.tgz",
- "integrity": "sha512-BHfv1h7P9/SyvcDJDaRuIwDu2yrDLlXlYmjvaLZTtPw6Ok/ZVhBR55JqW832XN/Fsl6k3LjdkYHHR7xnsa5Wvg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.0.0.tgz",
+ "integrity": "sha512-oOoQhSpg/RGiGHjn/cqtYpHBkkd+5M/DCi1jmfW+ZOvLVx21QVt6PbWIJoKJF85moNFo4UG9pMBU35R1MaxUKQ==",
+ "peer": true,
"dependencies": {
- "@algolia/requester-common": "4.22.0"
+ "@algolia/client-common": "5.0.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
}
},
"node_modules/@algolia/requester-common": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.22.0.tgz",
- "integrity": "sha512-Y9cEH/cKjIIZgzvI1aI0ARdtR/xRrOR13g5psCxkdhpgRN0Vcorx+zePhmAa4jdQNqexpxtkUdcKYugBzMZJgQ=="
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz",
+ "integrity": "sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA=="
},
"node_modules/@algolia/requester-node-http": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.22.0.tgz",
- "integrity": "sha512-8xHoGpxVhz3u2MYIieHIB6MsnX+vfd5PS4REgglejJ6lPigftRhTdBCToe6zbwq4p0anZXjjPDvNWMlgK2+xYA==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.0.0.tgz",
+ "integrity": "sha512-FwCdugzpnW0wxbgWPauAz5vhmWGQnjZa5DCl9PBbIoDNEy/NIV8DmiL9CEA+LljQdDidG0l0ijojcTNaRRtPvQ==",
+ "peer": true,
"dependencies": {
- "@algolia/requester-common": "4.22.0"
+ "@algolia/client-common": "5.0.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
}
},
"node_modules/@algolia/transporter": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.22.0.tgz",
- "integrity": "sha512-ieO1k8x2o77GNvOoC+vAkFKppydQSVfbjM3YrSjLmgywiBejPTvU1R1nEvG59JIIUvtSLrZsLGPkd6vL14zopA==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz",
+ "integrity": "sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==",
"dependencies": {
- "@algolia/cache-common": "4.22.0",
- "@algolia/logger-common": "4.22.0",
- "@algolia/requester-common": "4.22.0"
+ "@algolia/cache-common": "4.24.0",
+ "@algolia/logger-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+ "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+ "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+ "engines": {
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.23.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
- "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+ "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+ "dependencies": {
+ "@babel/types": "^7.25.2"
+ },
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -175,28 +306,41 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/types": {
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+ "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.24.8",
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@docsearch/css": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.5.2.tgz",
- "integrity": "sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA=="
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.1.tgz",
+ "integrity": "sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg=="
},
"node_modules/@docsearch/js": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.5.2.tgz",
- "integrity": "sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==",
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.1.tgz",
+ "integrity": "sha512-erI3RRZurDr1xES5hvYJ3Imp7jtrXj6f1xYIzDzxiS7nNBufYWPbJwrmMqWC5g9y165PmxEmN9pklGCdLi0Iqg==",
"dependencies": {
- "@docsearch/react": "3.5.2",
+ "@docsearch/react": "3.6.1",
"preact": "^10.0.0"
}
},
"node_modules/@docsearch/react": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.5.2.tgz",
- "integrity": "sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==",
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.1.tgz",
+ "integrity": "sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==",
"dependencies": {
"@algolia/autocomplete-core": "1.9.3",
"@algolia/autocomplete-preset-algolia": "1.9.3",
- "@docsearch/css": "3.5.2",
+ "@docsearch/css": "3.6.1",
"algoliasearch": "^4.19.1"
},
"peerDependencies": {
@@ -221,9 +365,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
- "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
@@ -236,9 +380,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
- "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
@@ -251,9 +395,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
- "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
@@ -266,9 +410,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
- "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
@@ -281,9 +425,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
- "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
@@ -296,9 +440,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
- "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
@@ -311,9 +455,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
- "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
@@ -326,9 +470,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
- "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
@@ -341,9 +485,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
- "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
@@ -356,9 +500,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
- "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
@@ -371,9 +515,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
- "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
@@ -386,9 +530,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
- "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
@@ -401,9 +545,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
- "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
@@ -416,9 +560,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
- "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
@@ -431,9 +575,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
- "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
@@ -446,9 +590,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
- "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
@@ -461,9 +605,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
- "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
@@ -476,9 +620,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
- "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
@@ -491,9 +635,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
- "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
@@ -506,9 +650,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
- "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
@@ -521,9 +665,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
- "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
@@ -536,9 +680,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
- "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
@@ -551,9 +695,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
- "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
@@ -566,14 +710,14 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.3.tgz",
- "integrity": "sha512-nvh9bB41vXEoKKvlWCGptpGt8EhrEwPQFDCY0VAto+R+qpSbaErPS3OjMZuXR8i/2UVw952Dtlnl2JFxH31Qvg==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
+ "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
"cpu": [
"arm"
],
@@ -583,9 +727,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.3.tgz",
- "integrity": "sha512-kffYCJ2RhDL1DlshLzYPyJtVeusHlA8Q1j6k6s4AEVKLq/3HfGa2ADDycLsmPo3OW83r4XtOPqRMbcFzFsEIzQ==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
+ "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
"cpu": [
"arm64"
],
@@ -595,9 +739,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.3.tgz",
- "integrity": "sha512-Fo7DR6Q9/+ztTyMBZ79+WJtb8RWZonyCgkBCjV51rW5K/dizBzImTW6HLC0pzmHaAevwM0jW1GtB5LCFE81mSw==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
+ "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
"cpu": [
"arm64"
],
@@ -607,9 +751,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.3.tgz",
- "integrity": "sha512-5HcxDF9fqHucIlTiw/gmMb3Qv23L8bLCg904I74Q2lpl4j/20z9ogaD3tWkeguRuz+/17cuS321PT3PAuyjQdg==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
+ "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
"cpu": [
"x64"
],
@@ -619,9 +763,21 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.3.tgz",
- "integrity": "sha512-cO6hKV+99D1V7uNJQn1chWaF9EGp7qV2N8sGH99q9Y62bsbN6Il55EwJppEWT+JiqDRg396vWCgwdHwje8itBQ==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
+ "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
+ "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
"cpu": [
"arm"
],
@@ -631,9 +787,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.3.tgz",
- "integrity": "sha512-xANyq6lVg6KMO8UUs0LjA4q7di3tPpDbzLPgVEU2/F1ngIZ54eli8Zdt3uUUTMXVbgTCafIO+JPeGMhu097i3w==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
+ "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
"cpu": [
"arm64"
],
@@ -643,9 +799,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.3.tgz",
- "integrity": "sha512-TZJUfRTugVFATQToCMD8DNV6jv/KpSwhE1lLq5kXiQbBX3Pqw6dRKtzNkh5wcp0n09reBBq/7CGDERRw9KmE+g==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
+ "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
"cpu": [
"arm64"
],
@@ -654,10 +810,22 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
+ "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.3.tgz",
- "integrity": "sha512-4/QVaRyaB5tkEAGfjVvWrmWdPF6F2NoaoO5uEP7N0AyeBw7l8SeCWWKAGrbx/00PUdHrJVURJiYikazslSKttQ==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
+ "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
"cpu": [
"riscv64"
],
@@ -666,10 +834,22 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
+ "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.3.tgz",
- "integrity": "sha512-koLC6D3pj1YLZSkTy/jsk3HOadp7q2h6VQl/lPX854twOmmLNekHB6yuS+MkWcKdGGdW1JPuPBv/ZYhr5Yhtdg==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
+ "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
"cpu": [
"x64"
],
@@ -679,9 +859,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.3.tgz",
- "integrity": "sha512-0OAkQ4HBp+JO2ip2Lgt/ShlrveOMzyhwt2D0KvqH28jFPqfZco28KSq76zymZwmU+F6GRojdxtQMJiNSXKNzeA==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
+ "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
"cpu": [
"x64"
],
@@ -691,9 +871,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.3.tgz",
- "integrity": "sha512-z5uvoMvdRWggigOnsb9OOCLERHV0ykRZoRB5O+URPZC9zM3pkoMg5fN4NKu2oHqgkzZtfx9u4njqqlYEzM1v9A==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
+ "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
"cpu": [
"arm64"
],
@@ -703,9 +883,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.3.tgz",
- "integrity": "sha512-wxomCHjBVKws+O4N1WLnniKCXu7vkLtdq9Fl9CN/EbwEldojvUrkoHE/fBLZzC7IT/x12Ut6d6cRs4dFvqJkMg==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
+ "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
"cpu": [
"ia32"
],
@@ -715,9 +895,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.3.tgz",
- "integrity": "sha512-1Qf/qk/iEtx0aOi+AQQt5PBoW0mFngsm7bPuxHClC/hWh2hHBktR6ktSfUg5b5rC9v8hTwNmHE7lBWXkgqluUQ==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
+ "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
"cpu": [
"x64"
],
@@ -726,29 +906,58 @@
"win32"
]
},
+ "node_modules/@shikijs/core": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.13.0.tgz",
+ "integrity": "sha512-Mj5NVfbAXcD1GnwOTSPl8hBn/T8UDpfFQTptp+p41n/CbUcJtOq98WaRD7Lz3hCglYotUTHUWtzu3JhK6XlkAA==",
+ "dependencies": {
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/@shikijs/transformers": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.13.0.tgz",
+ "integrity": "sha512-51aLIT6a93rVGoTxl2+p6hb7ILbTA4p/unoibEAjnPMzHto4cqxhuHyDVgtQur5ANpGsL3ihSGKaZDrpcWH8vQ==",
+ "dependencies": {
+ "shiki": "1.13.0"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/linkify-it": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
- "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
},
"node_modules/@types/markdown-it": {
- "version": "13.0.7",
- "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz",
- "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dependencies": {
- "@types/linkify-it": "*",
- "@types/mdurl": "*"
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
- "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
@@ -756,9 +965,9 @@
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@vitejs/plugin-vue": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz",
- "integrity": "sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz",
+ "integrity": "sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==",
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
@@ -768,118 +977,144 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.5.tgz",
- "integrity": "sha512-Daka7P1z2AgKjzuueWXhwzIsKu0NkLB6vGbNVEV2iJ8GJTrzraZo/Sk4GWCMRtd/qVi3zwnk+Owbd/xSZbwHtQ==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz",
+ "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==",
"dependencies": {
- "@babel/parser": "^7.23.6",
- "@vue/shared": "3.4.5",
+ "@babel/parser": "^7.24.7",
+ "@vue/shared": "3.4.38",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.5.tgz",
- "integrity": "sha512-J8YlxknJVd90SXFJ4HwGANSAXsx5I0lK30sO/zvYV7s5gXf7gZR7r/1BmZ2ju7RGH1lnc6bpBc6nL61yW+PsAQ==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz",
+ "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==",
"dependencies": {
- "@vue/compiler-core": "3.4.5",
- "@vue/shared": "3.4.5"
+ "@vue/compiler-core": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.5.tgz",
- "integrity": "sha512-jauvkDuSSUbP0ebhfNqljhShA90YEfX/0wZ+w40oZF43IjGyWYjqYaJbvMJwGOd+9+vODW6eSvnk28f0SGV7OQ==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz",
+ "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==",
"dependencies": {
- "@babel/parser": "^7.23.6",
- "@vue/compiler-core": "3.4.5",
- "@vue/compiler-dom": "3.4.5",
- "@vue/compiler-ssr": "3.4.5",
- "@vue/shared": "3.4.5",
+ "@babel/parser": "^7.24.7",
+ "@vue/compiler-core": "3.4.38",
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/compiler-ssr": "3.4.38",
+ "@vue/shared": "3.4.38",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.5",
- "postcss": "^8.4.32",
- "source-map-js": "^1.0.2"
+ "magic-string": "^0.30.10",
+ "postcss": "^8.4.40",
+ "source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.5.tgz",
- "integrity": "sha512-DDdEcDzj2lWTMfUMMtEpLDhURai9LhM0zSZ219jCt7b2Vyl0/jy3keFgCPMitG0V1S1YG4Cmws3lWHWdxHQOpg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz",
+ "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==",
"dependencies": {
- "@vue/compiler-dom": "3.4.5",
- "@vue/shared": "3.4.5"
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/devtools-api": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz",
- "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA=="
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.3.8.tgz",
+ "integrity": "sha512-NURFwmxz4WukFU54IHgyGI2KSejdgHG5JC4xTcWmTWEBIc8aelj9fBy4qsboObGHFp3JIdRxxANO9s2wZA/pVQ==",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.3.8"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.8.tgz",
+ "integrity": "sha512-HYy3MQP1nZ6GbE4vrgJ/UB+MvZnhYmEwCa/UafrEpdpwa+jNCkz1ZdUrC5I7LpkH1ShREEV2/pZlAQdBj+ncLQ==",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.3.8",
+ "birpc": "^0.2.17",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.1"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.8.tgz",
+ "integrity": "sha512-1NiJbn7Yp47nPDWhFZyEKpB2+5/+7JYv8IQnU0ccMrgslPR2dL7u1DIyI7mLqy4HN1ll36gQy0k8GqBYSFgZJw==",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
},
"node_modules/@vue/reactivity": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.5.tgz",
- "integrity": "sha512-BcWkKvjdvqJwb7BhhFkXPLDCecX4d4a6GATvCduJQDLv21PkPowAE5GKuIE5p6RC07/Lp9FMkkq4AYCTVF5KlQ==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.38.tgz",
+ "integrity": "sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==",
"dependencies": {
- "@vue/shared": "3.4.5"
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.5.tgz",
- "integrity": "sha512-wh9ELIOQKeWT9SaUPdLrsxRkZv14jp+SJm9aiQGWio+/MWNM3Lib0wE6CoKEqQ9+SCYyGjDBhTOTtO47kCgbkg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.38.tgz",
+ "integrity": "sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==",
"dependencies": {
- "@vue/reactivity": "3.4.5",
- "@vue/shared": "3.4.5"
+ "@vue/reactivity": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.5.tgz",
- "integrity": "sha512-n5ewvOjyG3IEpqGBahdPXODFSpVlSz3H4LF76Sx0XAqpIOqyJ5bIb2PrdYuH2ogBMAQPh+o5tnoH4nJpBr8U0Q==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.38.tgz",
+ "integrity": "sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==",
"dependencies": {
- "@vue/runtime-core": "3.4.5",
- "@vue/shared": "3.4.5",
+ "@vue/reactivity": "3.4.38",
+ "@vue/runtime-core": "3.4.38",
+ "@vue/shared": "3.4.38",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.5.tgz",
- "integrity": "sha512-jOFc/VE87yvifQpNju12VcqimH8pBLxdcT+t3xMeiED1K6DfH9SORyhFEoZlW5TG2Vwfn3Ul5KE+1aC99xnSBg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.38.tgz",
+ "integrity": "sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==",
"dependencies": {
- "@vue/compiler-ssr": "3.4.5",
- "@vue/shared": "3.4.5"
+ "@vue/compiler-ssr": "3.4.38",
+ "@vue/shared": "3.4.38"
},
"peerDependencies": {
- "vue": "3.4.5"
+ "vue": "3.4.38"
}
},
"node_modules/@vue/shared": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.5.tgz",
- "integrity": "sha512-6XptuzlMvN4l4cDnDw36pdGEV+9njYkQ1ZE0Q6iZLwrKefKaOJyiFmcP3/KBDHbt72cJZGtllAc1GaHe6XGAyg=="
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz",
+ "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw=="
},
"node_modules/@vueuse/core": {
- "version": "10.7.1",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.1.tgz",
- "integrity": "sha512-74mWHlaesJSWGp1ihg76vAnfVq9NTv1YT0SYhAQ6zwFNdBkkP+CKKJmVOEHcdSnLXCXYiL5e7MaewblfiYLP7g==",
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.0.0.tgz",
+ "integrity": "sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
- "@vueuse/metadata": "10.7.1",
- "@vueuse/shared": "10.7.1",
- "vue-demi": ">=0.14.6"
+ "@vueuse/metadata": "11.0.0",
+ "@vueuse/shared": "11.0.0",
+ "vue-demi": ">=0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
- "version": "0.14.6",
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
- "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
@@ -902,30 +1137,30 @@
}
},
"node_modules/@vueuse/integrations": {
- "version": "10.7.1",
- "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.7.1.tgz",
- "integrity": "sha512-cKo5LEeKVHdBRBtMTOrDPdR0YNtrmN9IBfdcnY2P3m5LHVrsD0xiHUtAH1WKjHQRIErZG6rJUa6GA4tWZt89Og==",
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.0.0.tgz",
+ "integrity": "sha512-B95nBX4B2q2ZETBDldrKARM/fYXBHfwdo44UbHBq4bUTi25lrlc8MwAZGqEoRvdV4ND9T6O1Rb9e4kaCJFXnqw==",
"dependencies": {
- "@vueuse/core": "10.7.1",
- "@vueuse/shared": "10.7.1",
- "vue-demi": ">=0.14.6"
+ "@vueuse/core": "11.0.0",
+ "@vueuse/shared": "11.0.0",
+ "vue-demi": ">=0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
- "async-validator": "*",
- "axios": "*",
- "change-case": "*",
- "drauu": "*",
- "focus-trap": "*",
- "fuse.js": "*",
- "idb-keyval": "*",
- "jwt-decode": "*",
- "nprogress": "*",
- "qrcode": "*",
- "sortablejs": "*",
- "universal-cookie": "*"
+ "async-validator": "^4",
+ "axios": "^1",
+ "change-case": "^5",
+ "drauu": "^0.4",
+ "focus-trap": "^7",
+ "fuse.js": "^7",
+ "idb-keyval": "^6",
+ "jwt-decode": "^4",
+ "nprogress": "^0.2",
+ "qrcode": "^1.5",
+ "sortablejs": "^1",
+ "universal-cookie": "^7"
},
"peerDependenciesMeta": {
"async-validator": {
@@ -967,9 +1202,9 @@
}
},
"node_modules/@vueuse/integrations/node_modules/vue-demi": {
- "version": "0.14.6",
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
- "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
@@ -992,28 +1227,28 @@
}
},
"node_modules/@vueuse/metadata": {
- "version": "10.7.1",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.1.tgz",
- "integrity": "sha512-jX8MbX5UX067DYVsbtrmKn6eG6KMcXxLRLlurGkZku5ZYT3vxgBjui2zajvUZ18QLIjrgBkFRsu7CqTAg18QFw==",
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.0.0.tgz",
+ "integrity": "sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
- "version": "10.7.1",
- "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.1.tgz",
- "integrity": "sha512-v0jbRR31LSgRY/C5i5X279A/WQjD6/JsMzGa+eqt658oJ75IvQXAeONmwvEMrvJQKnRElq/frzBR7fhmWY5uLw==",
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.0.0.tgz",
+ "integrity": "sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==",
"dependencies": {
- "vue-demi": ">=0.14.6"
+ "vue-demi": ">=0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
- "version": "0.14.6",
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
- "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
@@ -1036,24 +1271,82 @@
}
},
"node_modules/algoliasearch": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.22.0.tgz",
- "integrity": "sha512-gfceltjkwh7PxXwtkS8KVvdfK+TSNQAWUeNSxf4dA29qW5tf2EGwa8jkJujlT9jLm17cixMVoGNc+GJFO1Mxhg==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.24.0.tgz",
+ "integrity": "sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==",
+ "dependencies": {
+ "@algolia/cache-browser-local-storage": "4.24.0",
+ "@algolia/cache-common": "4.24.0",
+ "@algolia/cache-in-memory": "4.24.0",
+ "@algolia/client-account": "4.24.0",
+ "@algolia/client-analytics": "4.24.0",
+ "@algolia/client-common": "4.24.0",
+ "@algolia/client-personalization": "4.24.0",
+ "@algolia/client-search": "4.24.0",
+ "@algolia/logger-common": "4.24.0",
+ "@algolia/logger-console": "4.24.0",
+ "@algolia/recommend": "4.24.0",
+ "@algolia/requester-browser-xhr": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/requester-node-http": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/algoliasearch/node_modules/@algolia/client-common": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz",
+ "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/algoliasearch/node_modules/@algolia/client-search": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz",
+ "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==",
+ "dependencies": {
+ "@algolia/client-common": "4.24.0",
+ "@algolia/requester-common": "4.24.0",
+ "@algolia/transporter": "4.24.0"
+ }
+ },
+ "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz",
+ "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==",
+ "dependencies": {
+ "@algolia/requester-common": "4.24.0"
+ }
+ },
+ "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz",
+ "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==",
"dependencies": {
- "@algolia/cache-browser-local-storage": "4.22.0",
- "@algolia/cache-common": "4.22.0",
- "@algolia/cache-in-memory": "4.22.0",
- "@algolia/client-account": "4.22.0",
- "@algolia/client-analytics": "4.22.0",
- "@algolia/client-common": "4.22.0",
- "@algolia/client-personalization": "4.22.0",
- "@algolia/client-search": "4.22.0",
- "@algolia/logger-common": "4.22.0",
- "@algolia/logger-console": "4.22.0",
- "@algolia/requester-browser-xhr": "4.22.0",
- "@algolia/requester-common": "4.22.0",
- "@algolia/requester-node-http": "4.22.0",
- "@algolia/transporter": "4.22.0"
+ "@algolia/requester-common": "4.24.0"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.17.tgz",
+ "integrity": "sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/copy-anything": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": {
@@ -1073,9 +1366,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.19.11",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
- "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -1084,29 +1377,29 @@
"node": ">=12"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.19.11",
- "@esbuild/android-arm": "0.19.11",
- "@esbuild/android-arm64": "0.19.11",
- "@esbuild/android-x64": "0.19.11",
- "@esbuild/darwin-arm64": "0.19.11",
- "@esbuild/darwin-x64": "0.19.11",
- "@esbuild/freebsd-arm64": "0.19.11",
- "@esbuild/freebsd-x64": "0.19.11",
- "@esbuild/linux-arm": "0.19.11",
- "@esbuild/linux-arm64": "0.19.11",
- "@esbuild/linux-ia32": "0.19.11",
- "@esbuild/linux-loong64": "0.19.11",
- "@esbuild/linux-mips64el": "0.19.11",
- "@esbuild/linux-ppc64": "0.19.11",
- "@esbuild/linux-riscv64": "0.19.11",
- "@esbuild/linux-s390x": "0.19.11",
- "@esbuild/linux-x64": "0.19.11",
- "@esbuild/netbsd-x64": "0.19.11",
- "@esbuild/openbsd-x64": "0.19.11",
- "@esbuild/sunos-x64": "0.19.11",
- "@esbuild/win32-arm64": "0.19.11",
- "@esbuild/win32-ia32": "0.19.11",
- "@esbuild/win32-x64": "0.19.11"
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/estree-walker": {
@@ -1135,15 +1428,28 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/magic-string": {
- "version": "0.30.5",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
- "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
+ "version": "0.30.11",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+ "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.15"
- },
- "engines": {
- "node": ">=12"
+ "@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/mark.js": {
@@ -1152,9 +1458,14 @@
"integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="
},
"node_modules/minisearch": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz",
- "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ=="
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz",
+ "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA=="
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/nanoid": {
"version": "3.3.7",
@@ -1173,15 +1484,20 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/picocolors": {
+ "node_modules/perfect-debounce": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
},
"node_modules/postcss": {
- "version": "8.4.33",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
- "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+ "version": "8.4.41",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
+ "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"funding": [
{
"type": "opencollective",
@@ -1198,26 +1514,31 @@
],
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "picocolors": "^1.0.1",
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/preact": {
- "version": "10.19.3",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz",
- "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==",
+ "version": "10.23.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.23.2.tgz",
+ "integrity": "sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
+ },
"node_modules/rollup": {
- "version": "4.9.3",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.3.tgz",
- "integrity": "sha512-JnchF0ZGFiqGpAPjg3e89j656Ne4tTtCY1VZc1AxtoQcRIxjTu9jyYHBAtkDXE+X681n4un/nX9SU52AroSRzg==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
+ "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -1229,70 +1550,88 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.9.3",
- "@rollup/rollup-android-arm64": "4.9.3",
- "@rollup/rollup-darwin-arm64": "4.9.3",
- "@rollup/rollup-darwin-x64": "4.9.3",
- "@rollup/rollup-linux-arm-gnueabihf": "4.9.3",
- "@rollup/rollup-linux-arm64-gnu": "4.9.3",
- "@rollup/rollup-linux-arm64-musl": "4.9.3",
- "@rollup/rollup-linux-riscv64-gnu": "4.9.3",
- "@rollup/rollup-linux-x64-gnu": "4.9.3",
- "@rollup/rollup-linux-x64-musl": "4.9.3",
- "@rollup/rollup-win32-arm64-msvc": "4.9.3",
- "@rollup/rollup-win32-ia32-msvc": "4.9.3",
- "@rollup/rollup-win32-x64-msvc": "4.9.3",
+ "@rollup/rollup-android-arm-eabi": "4.20.0",
+ "@rollup/rollup-android-arm64": "4.20.0",
+ "@rollup/rollup-darwin-arm64": "4.20.0",
+ "@rollup/rollup-darwin-x64": "4.20.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.20.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.20.0",
+ "@rollup/rollup-linux-arm64-musl": "4.20.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.20.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.20.0",
+ "@rollup/rollup-linux-x64-gnu": "4.20.0",
+ "@rollup/rollup-linux-x64-musl": "4.20.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.20.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.20.0",
+ "@rollup/rollup-win32-x64-msvc": "4.20.0",
"fsevents": "~2.3.2"
}
},
"node_modules/search-insights": {
- "version": "2.13.0",
- "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz",
- "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==",
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.16.3.tgz",
+ "integrity": "sha512-hSHy/s4Zk2xibhj9XTCACB+1PqS+CaJxepGNBhKc/OsHRpqvHAUAm5+uZ6kJJbGXn0pb3XqekHjg6JAqPExzqg==",
"peer": true
},
- "node_modules/shikiji": {
- "version": "0.9.17",
- "resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.9.17.tgz",
- "integrity": "sha512-0z/1NfkhBkm3ijrfFeHg3G9yDNuHhXdAGbQm7tRxj4WQ5z2y0XDbnagFyKyuV2ebCTS1Mwy1I3n0Fzcc/4xdmw==",
+ "node_modules/shiki": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.13.0.tgz",
+ "integrity": "sha512-e0dWfnONbEv6xl7FJy3XIhsVHQ/65XHDZl92+6H9+4xWjfdo7pmkqG7Kg47KWtDiEtzM5Z+oEfb4vtRvoZ/X9w==",
"dependencies": {
- "shikiji-core": "0.9.17"
+ "@shikijs/core": "1.13.0",
+ "@types/hast": "^3.0.4"
}
},
- "node_modules/shikiji-core": {
- "version": "0.9.17",
- "resolved": "https://registry.npmjs.org/shikiji-core/-/shikiji-core-0.9.17.tgz",
- "integrity": "sha512-r1FWTXk6SO2aYqfWgcsJ11MuVQ1ymPSdXzJjK7q8EXuyqu8yc2N5qrQy5+BL6gTVOaF4yLjbxFjF+KTRM1Sp8Q=="
- },
- "node_modules/shikiji-transformers": {
- "version": "0.9.17",
- "resolved": "https://registry.npmjs.org/shikiji-transformers/-/shikiji-transformers-0.9.17.tgz",
- "integrity": "sha512-2CCG9qSLS6Bn/jbeUTEuvC6YSuP8gm8VyX5VjmCvDKyCPGhlLJbH1k/kg9wfRt7cJqpYjhdMDgT5rkdYrOZnsA==",
- "dependencies": {
- "shikiji": "0.9.17"
+ "node_modules/source-map-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/superjson": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz",
+ "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/vite": {
- "version": "5.0.11",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz",
- "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==",
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz",
+ "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==",
"dependencies": {
- "esbuild": "^0.19.3",
- "postcss": "^8.4.32",
- "rollup": "^4.2.0"
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.41",
+ "rollup": "^4.13.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -1311,6 +1650,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
+ "sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -1328,6 +1668,9 @@
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
},
@@ -1340,32 +1683,33 @@
}
},
"node_modules/vitepress": {
- "version": "1.0.0-rc.35",
- "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-rc.35.tgz",
- "integrity": "sha512-+2VnFwtYIiKWWAnMjWg7ik0PfsUdrNoZIZKeu5dbJtrkzKO/mTvlA3owiT5VBKJsZAgI17B5UV37aYfUvGrN6g==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.3.3.tgz",
+ "integrity": "sha512-6UzEw/wZ41S/CATby7ea7UlffvRER/uekxgN6hbEvSys9ukmLOKsz87Ehq9yOx1Rwiw+Sj97yjpivP8w1sUmng==",
"dependencies": {
- "@docsearch/css": "^3.5.2",
- "@docsearch/js": "^3.5.2",
- "@types/markdown-it": "^13.0.7",
- "@vitejs/plugin-vue": "^5.0.2",
- "@vue/devtools-api": "^6.5.1",
- "@vueuse/core": "^10.7.1",
- "@vueuse/integrations": "^10.7.1",
+ "@docsearch/css": "^3.6.1",
+ "@docsearch/js": "^3.6.1",
+ "@shikijs/core": "^1.13.0",
+ "@shikijs/transformers": "^1.13.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^5.1.2",
+ "@vue/devtools-api": "^7.3.8",
+ "@vue/shared": "^3.4.38",
+ "@vueuse/core": "^11.0.0",
+ "@vueuse/integrations": "^11.0.0",
"focus-trap": "^7.5.4",
"mark.js": "8.11.1",
- "minisearch": "^6.3.0",
- "shikiji": "^0.9.17",
- "shikiji-core": "^0.9.17",
- "shikiji-transformers": "^0.9.17",
- "vite": "^5.0.10",
- "vue": "^3.4.4"
+ "minisearch": "^7.1.0",
+ "shiki": "^1.13.0",
+ "vite": "^5.4.1",
+ "vue": "^3.4.38"
},
"bin": {
"vitepress": "bin/vitepress.js"
},
"peerDependencies": {
- "markdown-it-mathjax3": "^4.3.2",
- "postcss": "^8.4.32"
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
},
"peerDependenciesMeta": {
"markdown-it-mathjax3": {
@@ -1377,15 +1721,15 @@
}
},
"node_modules/vue": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.5.tgz",
- "integrity": "sha512-VH6nHFhLPjgu2oh5vEBXoNZxsGHuZNr3qf4PHClwJWw6IDqw6B3x+4J+ABdoZ0aJuT8Zi0zf3GpGlLQCrGWHrw==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz",
+ "integrity": "sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==",
"dependencies": {
- "@vue/compiler-dom": "3.4.5",
- "@vue/compiler-sfc": "3.4.5",
- "@vue/runtime-dom": "3.4.5",
- "@vue/server-renderer": "3.4.5",
- "@vue/shared": "3.4.5"
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/compiler-sfc": "3.4.38",
+ "@vue/runtime-dom": "3.4.38",
+ "@vue/server-renderer": "3.4.38",
+ "@vue/shared": "3.4.38"
},
"peerDependencies": {
"typescript": "*"
diff --git a/docs/package.json b/docs/package.json
index f66494b..1e1eb7a 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,6 +5,6 @@
"docs:preview": "vitepress preview"
},
"dependencies": {
- "vitepress": "^1.0.0-rc.35"
+ "vitepress": "^1.3.3"
}
}
diff --git a/internal/clip/clip.go b/internal/clip/clip.go
index 9904e14..74e0059 100644
--- a/internal/clip/clip.go
+++ b/internal/clip/clip.go
@@ -42,6 +42,22 @@ func DotProductFloat32Float32(a []float32, b []float32) (float32, error) {
return dot, nil
}
+func CosineSimilarityEmbeddingFloat32(e Embedding, f []float32, invnorm float32) (float32, error) {
+ dot, err := DotProductFloat32Float32(e.Float32(), f)
+ if err != nil {
+ return 0, err
+ }
+ return dot * invnorm * e.InvNormFloat32(), nil
+}
+
+func CosineSimilarityFloat32Float32(a []float32, ainvnorm float32, b []float32, binvnorm float32) (float32, error) {
+ dot, err := DotProductFloat32Float32(a, b)
+ if err != nil {
+ return 0, err
+ }
+ return dot * ainvnorm * binvnorm, nil
+}
+
// Most real world inverse vector norms of embeddings fall
// within ~500 of 11843, so it's more efficient to store
// the inverse vector norm as an offset of this number.
diff --git a/internal/codec/image.go b/internal/codec/image.go
index 840331b..eaf2c45 100644
--- a/internal/codec/image.go
+++ b/internal/codec/image.go
@@ -13,8 +13,8 @@ func DecodeJpeg(reader io.ReadSeeker) (image.Image, error) {
return jpeg.Decode(reader)
}
-func EncodeJpeg(w io.Writer, image image.Image) error {
+func EncodeJpeg(w io.Writer, image image.Image, quality int) error {
return jpeg.Encode(w, image, &jpeg.Options{
- Quality: 80,
+ Quality: quality,
})
}
diff --git a/internal/image/database.go b/internal/image/database.go
index e6a6e9d..3693716 100644
--- a/internal/image/database.go
+++ b/internal/image/database.go
@@ -38,9 +38,11 @@ const (
)
type ListOptions struct {
- OrderBy ListOrder
- Limit int
- Query *search.Query
+ OrderBy ListOrder
+ Limit int
+ Query *search.Query
+ Embedding clip.Embedding
+ Extensions []string
}
type DirsFunc func(dirs []string)
@@ -1374,8 +1376,32 @@ func (source *Database) List(dirs []string, options ListOptions) <-chan InfoList
}
}
+ joinEmbeddings := false
+ var emb []float32
+ var embInvNorm float32
+ if options.Embedding != nil {
+ emb = options.Embedding.Float32()
+ embInvNorm = options.Embedding.InvNormFloat32()
+ }
+
+ embThreshold := float32(0)
+ if f, err := options.Query.QualifierFloat32("t"); err == nil {
+ embThreshold = f
+ joinEmbeddings = true
+ }
+
+ embDedup := float32(0)
+ if f, err := options.Query.QualifierFloat32("dedup"); err == nil {
+ embDedup = f
+ joinEmbeddings = true
+ }
+
+ sql += `
+ SELECT infos.id, width, height, orientation, color, created_at_unix, created_at_tz_offset, latitude, longitude`
+ if joinEmbeddings {
+ sql += `, inv_norm, embedding`
+ }
sql += `
- SELECT infos.id, width, height, orientation, color, created_at_unix, created_at_tz_offset, latitude, longitude
FROM infos
`
@@ -1387,6 +1413,12 @@ func (source *Database) List(dirs []string, options ListOptions) <-chan InfoList
}
}
+ if joinEmbeddings {
+ sql += `
+ LEFT JOIN clip_emb ON clip_emb.file_id = id
+ `
+ }
+
sql += `
WHERE path_prefix_id IN (
SELECT id
@@ -1404,6 +1436,29 @@ func (source *Database) List(dirs []string, options ListOptions) <-chan InfoList
)
`
+ if len(options.Extensions) > 0 {
+ sql += `
+ AND (
+ `
+ for i := range options.Extensions {
+ sql += `filename LIKE ? `
+ if i < len(options.Extensions)-1 {
+ sql += "OR "
+ }
+ }
+ sql += `
+ )
+ `
+ }
+
+ createdFrom, createdTo, createdErr := options.Query.QualifierDateRange("created")
+ if createdErr == nil {
+ sql += `
+ AND created_at_unix >= ?
+ AND created_at_unix <= ?
+ `
+ }
+
switch options.OrderBy {
case None:
case DateAsc:
@@ -1441,10 +1496,25 @@ func (source *Database) List(dirs []string, options ListOptions) <-chan InfoList
bindIndex++
}
+ for _, ext := range options.Extensions {
+ stmt.BindText(bindIndex, "%"+ext)
+ bindIndex++
+ }
+
+ if createdErr == nil {
+ stmt.BindInt64(bindIndex, createdFrom.Unix())
+ bindIndex++
+ stmt.BindInt64(bindIndex, createdTo.Unix())
+ bindIndex++
+ }
+
if options.Limit > 0 {
stmt.BindInt64(bindIndex, (int64)(options.Limit))
}
+ var lastEmb []float32
+ var lastEmbInvNorm float32
+
for {
if exists, err := stmt.Step(); err != nil {
log.Printf("Error listing files: %s\n", err.Error())
@@ -1478,6 +1548,40 @@ func (source *Database) List(dirs []string, options ListOptions) <-chan InfoList
info.LatLng = s2.LatLngFromDegrees(stmt.ColumnFloat(7), stmt.ColumnFloat(8))
}
+ if joinEmbeddings {
+ e, err := readEmbedding(stmt, 9, 10)
+ if err != nil {
+ continue
+ }
+ ee := e.Float32()
+ einv := e.InvNormFloat32()
+ if emb != nil {
+ sim, err := clip.CosineSimilarityFloat32Float32(emb, embInvNorm, ee, einv)
+ if err != nil {
+ log.Printf("Error calculating similarity for %d: %v\n", info.Id, err)
+ continue
+ }
+ // fmt.Printf("id %d sim %f %f\n", info.Id, sim, embThreshold)
+ if sim < embThreshold {
+ continue
+ }
+ }
+ if embDedup > 0 {
+ if lastEmb != nil {
+ sim, err := clip.CosineSimilarityFloat32Float32(lastEmb, lastEmbInvNorm, ee, einv)
+ if err != nil {
+ log.Printf("Error calculating similarity for %d: %v\n", info.Id, err)
+ continue
+ }
+ if sim >= embDedup {
+ continue
+ }
+ }
+ lastEmb = ee
+ lastEmbInvNorm = einv
+ }
+ }
+
out <- info
}
diff --git a/internal/image/exiftool-mostlygeek.go b/internal/image/exiftool-mostlygeek.go
index 7b9466d..5b475f7 100644
--- a/internal/image/exiftool-mostlygeek.go
+++ b/internal/image/exiftool-mostlygeek.go
@@ -90,7 +90,7 @@ func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) ([]
}
name := strings.TrimSpace(nameValueSplit[0])
value := strings.TrimSpace(nameValueSplit[1])
- // println(name, value)
+ // println(path, name, value)
switch name {
case "Orientation":
orientation = value
@@ -104,22 +104,34 @@ func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) ([]
latitude = value
case "GPSLongitude":
longitude = value
-
- // case "GPSDateTime":
- // gpsTime, _ = parseDateTime(value)
default:
if name, ok := tag.ExifTagToName[name]; ok {
tags = append(tags, tag.NewExif(name, value))
}
if strings.Contains(name, "Date") || strings.Contains(name, "Time") {
if info.DateTime.IsZero() {
- info.DateTime, _, _, _ = parseDateTime(value)
- } else if name != "GPSDateTime" && name != "FileModifyDate" && name != "FileCreateDate" {
- // Prefer time with timezone if available
- t, hasTimezone, _, _ := parseDateTime(value)
- if hasTimezone && info.DateTime.Location() == time.UTC {
+ t, _, _, err := parseDateTime(value)
+ if err == nil {
info.DateTime = t
}
+ } else if name != "GPSDateTime" && name != "FileModifyDate" && name != "FileCreateDate" {
+ t, hasTimezone, _, err := parseDateTime(value)
+ if err == nil && info.DateTime.Location() == time.UTC {
+ if hasTimezone {
+ // Prefer time with timezone if available
+ info.DateTime = t
+ } else {
+ // If there are two times that are more than 10 minutes apart and
+ // the first one doesn't have a timezone, it's likely that the
+ // first one is local time and the second one is UTC, so we add
+ // the timezone to the first one.
+ d := info.DateTime.Sub(t)
+ if d.Abs() > 10*time.Minute {
+ d = d.Truncate(time.Minute)
+ info.DateTime = info.DateTime.Add(-d).In(time.FixedZone("", int(d.Seconds())))
+ }
+ }
+ }
}
} else if strings.HasSuffix(name, "Image") {
match := previewValueMatcher.FindStringSubmatch(value)
diff --git a/internal/image/info.go b/internal/image/info.go
index 252f7a3..d353920 100644
--- a/internal/image/info.go
+++ b/internal/image/info.go
@@ -39,6 +39,13 @@ func AngleToKm(a s1.Angle) float64 {
return a.Radians() * earthRadiusKm
}
+func (info *Info) MakeValid() {
+ if info.Width == 0 || info.Height == 0 {
+ info.Width = 3
+ info.Height = 2
+ }
+}
+
func (info *Info) Size() Size {
return Size{X: info.Width, Y: info.Height}
}
diff --git a/internal/image/source.go b/internal/image/source.go
index b4a1502..bbb318e 100644
--- a/internal/image/source.go
+++ b/internal/image/source.go
@@ -109,7 +109,7 @@ func (config *CacheConfig) MaxSizeBytes() int64 {
}
type Caches struct {
- Image CacheConfig
+ Image CacheConfig `json:"image"`
}
type Config struct {
@@ -198,7 +198,7 @@ func NewSource(config Config, migrations embed.FS, migrationsThumbs embed.FS, ge
[]string{"source"},
)
- source.imageCache = ristretto.New()
+ source.imageCache = ristretto.New(config.Caches.Image.MaxSizeBytes())
env := SourceEnvironment{
SourceTypes: config.SourceTypes,
FFmpegPath: ffmpeg.FindPath(),
@@ -393,9 +393,7 @@ func (source *Source) ListInfos(dirs []string, options ListOptions) <-chan Sourc
infos := source.database.List(dirs, options)
for info := range infos {
- // if info.NeedsMeta() || info.NeedsColor() {
- // info.Info = source.GetInfo(info.Id)
- // }
+ info.SourcedInfo.Info.MakeValid()
out <- info.SourcedInfo
}
close(out)
@@ -413,6 +411,7 @@ func (source *Source) ListInfosEmb(dirs []string, options ListOptions) <-chan In
infos := source.database.ListWithEmbeddings(dirs, options)
for info := range infos {
+ info.SourcedInfo.Info.MakeValid()
out <- info
}
close(out)
@@ -420,26 +419,6 @@ func (source *Source) ListInfosEmb(dirs []string, options ListOptions) <-chan In
return out
}
-func (source *Source) ListInfosWithExistence(dirs []string, options ListOptions) <-chan SourcedInfo {
- for i := range dirs {
- dirs[i] = filepath.FromSlash(dirs[i])
- }
- out := make(chan SourcedInfo, 1000)
- go func() {
- defer metrics.Elapsed("list infos")()
-
- infos := source.database.List(dirs, options)
- for info := range infos {
- if info.NeedsMeta() || info.NeedsColor() {
- info.Info = source.GetInfo(info.Id)
- }
- out <- info.SourcedInfo
- }
- close(out)
- }()
- return out
-}
-
// Prefer using ImageId over this unless you absolutely need the path
func (source *Source) GetImagePath(id ImageId) (string, error) {
path, ok := source.pathCache.Get(id)
diff --git a/internal/layout/common.go b/internal/layout/common.go
index 2d7846f..c54c0dc 100644
--- a/internal/layout/common.go
+++ b/internal/layout/common.go
@@ -55,6 +55,7 @@ type Layout struct {
ImageHeight float64
ImageSpacing float64
LineSpacing float64
+ Tweaks string
}
type Section struct {
diff --git a/internal/layout/flex.go b/internal/layout/flex.go
index 986fd05..efe2757 100644
--- a/internal/layout/flex.go
+++ b/internal/layout/flex.go
@@ -9,6 +9,7 @@ import (
"photofield/internal/layout/dag"
"photofield/internal/metrics"
"photofield/internal/render"
+ "strings"
"time"
"github.com/gammazero/deque"
@@ -22,12 +23,16 @@ func LayoutFlex(infos <-chan image.SourcedInfo, layout Layout, scene *render.Sce
layout.LineSpacing = 0.02 * layout.ImageHeight
sceneMargin := 10.
+ topMargin := 64.
+ if strings.Contains(layout.Tweaks, "notopmargin") {
+ topMargin = 0
+ }
scene.Bounds.W = layout.ViewportWidth
rect := render.Rect{
X: sceneMargin,
- Y: sceneMargin + 64,
+ Y: sceneMargin + topMargin,
W: scene.Bounds.W - sceneMargin*2,
H: 0,
}
@@ -58,8 +63,9 @@ func LayoutFlex(infos <-chan image.SourcedInfo, layout Layout, scene *render.Sce
var prevLocTime time.Time
var prevLocation string
var prevAuxTime time.Time
+ nogeo := strings.Contains(layout.Tweaks, "nogeo")
for info := range infos {
- if source.Geo.Available() {
+ if !nogeo && source.Geo.Available() {
photoTime := info.DateTime
lastLocCheck := prevLocTime.Sub(photoTime)
if lastLocCheck < 0 {
@@ -96,10 +102,6 @@ func LayoutFlex(infos <-chan image.SourcedInfo, layout Layout, scene *render.Sce
}
}
}
- if info.Width == 0 || info.Height == 0 {
- info.Width = 3
- info.Height = 2
- }
photo := dag.Photo{
Id: info.Id,
AspectRatio: float32(info.Width) / float32(info.Height),
diff --git a/internal/layout/highlights.go b/internal/layout/highlights.go
index fec03a5..cd12971 100644
--- a/internal/layout/highlights.go
+++ b/internal/layout/highlights.go
@@ -133,11 +133,6 @@ func LayoutHighlights(infos <-chan image.InfoEmb, layout Layout, scene *render.S
}
prevEmb = emb
prevInvNorm = invnorm
-
- if info.Width == 0 || info.Height == 0 {
- info.Width = 3
- info.Height = 2
- }
photo := HighlightPhoto{
Photo: dag.Photo{
Id: info.Id,
diff --git a/internal/openapi/api.gen.go b/internal/openapi/api.gen.go
index a1edaa5..2f2d2bb 100644
--- a/internal/openapi/api.gen.go
+++ b/internal/openapi/api.gen.go
@@ -152,6 +152,7 @@ type SceneParams struct {
Layout LayoutType `json:"layout"`
Search *Search `json:"search,omitempty"`
Sort *Sort `json:"sort,omitempty"`
+ Tweaks *Tweaks `json:"tweaks,omitempty"`
ViewportHeight ViewportHeight `json:"viewport_height"`
ViewportWidth ViewportWidth `json:"viewport_width"`
}
@@ -208,6 +209,9 @@ type TaskType string
// TileCoord defines model for TileCoord.
type TileCoord int
+// Tweaks defines model for Tweaks.
+type Tweaks string
+
// ViewportHeight defines model for ViewportHeight.
type ViewportHeight float32
@@ -239,6 +243,7 @@ type GetScenesParams struct {
Layout *LayoutType `json:"layout,omitempty"`
Sort *Sort `json:"sort,omitempty"`
Search *Search `json:"search,omitempty"`
+ Tweaks *Tweaks `json:"tweaks,omitempty"`
}
// PostScenesJSONBody defines parameters for PostScenes.
@@ -273,6 +278,7 @@ type GetScenesSceneIdTilesParams struct {
SelectTag *string `json:"select_tag,omitempty"`
DebugOverdraw *bool `json:"debug_overdraw,omitempty"`
DebugThumbnails *bool `json:"debug_thumbnails,omitempty"`
+ QualityPreset *string `json:"quality_preset,omitempty"`
}
// GetTagsParams defines parameters for GetTags.
@@ -630,6 +636,17 @@ func (siw *ServerInterfaceWrapper) GetScenes(w http.ResponseWriter, r *http.Requ
return
}
+ // ------------- Optional query parameter "tweaks" -------------
+ if paramValue := r.URL.Query().Get("tweaks"); paramValue != "" {
+
+ }
+
+ err = runtime.BindQueryParameter("form", true, false, "tweaks", r.URL.Query(), ¶ms.Tweaks)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Invalid format for parameter tweaks: %s", err), http.StatusBadRequest)
+ return
+ }
+
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetScenes(w, r, params)
}
@@ -995,6 +1012,17 @@ func (siw *ServerInterfaceWrapper) GetScenesSceneIdTiles(w http.ResponseWriter,
return
}
+ // ------------- Optional query parameter "quality_preset" -------------
+ if paramValue := r.URL.Query().Get("quality_preset"); paramValue != "" {
+
+ }
+
+ err = runtime.BindQueryParameter("form", true, false, "quality_preset", r.URL.Query(), ¶ms.QualityPreset)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Invalid format for parameter quality_preset: %s", err), http.StatusBadRequest)
+ return
+ }
+
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetScenesSceneIdTiles(w, r, sceneId, params)
}
diff --git a/internal/render/bitmap.go b/internal/render/bitmap.go
index 6a0b4f1..f51069e 100644
--- a/internal/render/bitmap.go
+++ b/internal/render/bitmap.go
@@ -45,15 +45,131 @@ func fitInside(cw float64, ch float64, w float64, h float64) (float64, float64)
// return r
// }
-func (bitmap *Bitmap) DrawImage(rimg draw.Image, img goimage.Image, c *canvas.Context, scale float64) {
+func cropsBlackbarsOnly(img goimage.Image, crop goimage.Rectangle) bool {
bounds := img.Bounds()
- model := bitmap.Sprite.Rect.GetMatrixFitBoundsRotate(bounds, bitmap.Orientation)
+ sum := uint64(0)
+ maxBlack := uint64(0xC00)
+
+ // Horizontal top left black bar line
+ for x := 0; x < crop.Min.X; x++ {
+ c := img.At(x, bounds.Min.Y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("horizontal top left black bar line: %v\n", sum)
+
+ // Horizontal top right black bar line
+ for x := crop.Max.X; x < bounds.Max.X; x++ {
+ c := img.At(x, bounds.Min.Y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("horizontal top right black bar line: %v\n", sum)
+
+ // Horizontal bottom left black bar line
+ for x := 0; x < crop.Min.X; x++ {
+ c := img.At(x, bounds.Max.Y-1)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("horizontal bottom left black bar line: %v\n", sum)
+
+ // Horizontal bottom right black bar line
+ for x := crop.Max.X; x < bounds.Max.X; x++ {
+ c := img.At(x, bounds.Max.Y-1)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("horizontal bottom right black bar line: %v\n", sum)
+
+ // Vertical top left black bar line
+ for y := 0; y < crop.Min.Y; y++ {
+ c := img.At(bounds.Min.X, y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("vertical top left black bar line: %v\n", sum)
+
+ // Vertical top right black bar line
+ for y := crop.Max.Y; y < bounds.Max.Y; y++ {
+ c := img.At(bounds.Min.X, y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("vertical top right black bar line: %v\n", sum)
+
+ // Vertical bottom left black bar line
+ for y := 0; y < crop.Min.Y; y++ {
+ c := img.At(bounds.Max.X-1, y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("vertical bottom left black bar line: %v\n", sum)
+
+ // Vertical bottom right black bar line
+ for y := crop.Max.Y; y < bounds.Max.Y; y++ {
+ c := img.At(bounds.Max.X-1, y)
+ r, g, b, _ := c.RGBA()
+ sum += uint64((r + g + b)) / 3 / maxBlack
+ }
+ // fmt.Printf("vertical bottom right black bar line: %v\n", sum)
+
+ return sum < 2
+}
+
+func cropRect(bitmap *Bitmap, bounds goimage.Rectangle) goimage.Rectangle {
+ brect := Rect{W: float64(bounds.Dx()), H: float64(bounds.Dy())}
+ rect := bitmap.Sprite.Rect
+ if bitmap.Orientation.SwapsDimensions() {
+ rect.W, rect.H = rect.H, rect.W
+ }
+ cropr := rect.FitInside(brect)
+ croprect := goimage.Rectangle{
+ Min: goimage.Point{X: int(math.Round(cropr.X)), Y: int(math.Round(cropr.Y))},
+ Max: goimage.Point{X: int(math.Round(cropr.X + cropr.W)), Y: int(math.Round(cropr.Y + cropr.H))},
+ }
+ return croprect
+}
+
+func (bitmap *Bitmap) DrawImage(rimg draw.Image, img goimage.Image, c *canvas.Context, scale float64, hq bool) {
+ bounds := img.Bounds()
+
+ arb := float64(bounds.Dx()) / float64(bounds.Dy())
+ aro := float64(bitmap.Sprite.Rect.W) / float64(bitmap.Sprite.Rect.H)
+ ard := math.Abs(arb - aro)
+ crop := ard > 0.05
+ // cut = false
+
+ var croprect goimage.Rectangle
+ if crop {
+ croprect = cropRect(bitmap, bounds)
+ if !cropsBlackbarsOnly(img, croprect) {
+ crop = false
+ }
+ } else {
+ croprect = bounds
+ }
+
+ var model canvas.Matrix
+ if crop {
+ model = bitmap.Sprite.Rect.GetMatrixFillBoundsRotate(bounds, bitmap.Orientation)
+ } else {
+ model = bitmap.Sprite.Rect.GetMatrixFitBoundsRotate(bounds, bitmap.Orientation)
+ }
+
m := c.View().Mul(model.ScaleAbout(scale, scale, float64(bounds.Max.X)*0.5, float64(bounds.Max.Y)*0.5))
- renderImageFast(rimg, img, m)
+
+ var interp draw.Interpolator
+ if hq {
+ interp = draw.CatmullRom
+ } else {
+ interp = draw.ApproxBiLinear
+ }
+ renderImage(rimg, img, m, croprect, interp)
}
-func renderImageFast(rimg draw.Image, img goimage.Image, m canvas.Matrix) {
+func renderImage(rimg draw.Image, img goimage.Image, m canvas.Matrix, crop goimage.Rectangle, interpolator draw.Interpolator) {
bounds := img.Bounds()
origin := m.Dot(canvas.Point{X: 0, Y: float64(bounds.Size().Y)})
h := float64(rimg.Bounds().Size().Y)
@@ -61,44 +177,38 @@ func renderImageFast(rimg draw.Image, img goimage.Image, m canvas.Matrix) {
m[0][0], -m[0][1], origin.X,
-m[1][0], m[1][1], h - origin.Y,
}
- draw.ApproxBiLinear.Transform(rimg, aff3, img, bounds, draw.Src, nil)
+ interpolator.Transform(rimg, aff3, img, crop, draw.Src, nil)
}
-func renderImageFastBounds(rimg draw.Image, img goimage.Image, m canvas.Matrix, bounds goimage.Rectangle) {
+func renderImageFastCropped(rimg draw.Image, img goimage.Image, m canvas.Matrix, crop goimage.Rectangle) {
+ bounds := img.Bounds()
origin := m.Dot(canvas.Point{X: 0, Y: float64(bounds.Size().Y)})
h := float64(rimg.Bounds().Size().Y)
aff3 := f64.Aff3{
m[0][0], -m[0][1], origin.X,
-m[1][0], m[1][1], h - origin.Y,
}
- draw.ApproxBiLinear.Transform(rimg, aff3, img, bounds, draw.Src, nil)
+ draw.ApproxBiLinear.Transform(rimg, aff3, img, crop, draw.Src, nil)
}
-// TODO finish implementation
-func renderImageFastCropped(rimg draw.Image, img goimage.Image, m canvas.Matrix, crop Rect, modelTopLeft canvas.Point, modelBottomRight canvas.Point) {
+func renderImageFast(rimg draw.Image, img goimage.Image, m canvas.Matrix) {
bounds := img.Bounds()
- // bounds := goimage.Rect(0, 0, int(modelBounds.X), int(modelBounds.Y))
origin := m.Dot(canvas.Point{X: 0, Y: float64(bounds.Size().Y)})
h := float64(rimg.Bounds().Size().Y)
aff3 := f64.Aff3{
m[0][0], -m[0][1], origin.X,
-m[1][0], m[1][1], h - origin.Y,
}
- // croptl := m.Dot(canvas.Point{crop.X, crop.Y})
- // cropbr := m.Dot(canvas.Point{crop.X + crop.W, crop.Y + crop.H})
- // println(bounds.String(), crop.String(), croptl.String(), cropbr.String())
- // tx, ty := m.D
+ draw.ApproxBiLinear.Transform(rimg, aff3, img, bounds, draw.Src, nil)
+}
- model := Rect{
- X: modelTopLeft.X,
- Y: modelTopLeft.Y,
- W: modelBottomRight.X - modelTopLeft.X,
- H: modelBottomRight.Y - modelTopLeft.Y,
+func renderImageFastBounds(rimg draw.Image, img goimage.Image, m canvas.Matrix, bounds goimage.Rectangle) {
+ origin := m.Dot(canvas.Point{X: 0, Y: float64(bounds.Size().Y)})
+ h := float64(rimg.Bounds().Size().Y)
+ aff3 := f64.Aff3{
+ m[0][0], -m[0][1], origin.X,
+ -m[1][0], m[1][1], h - origin.Y,
}
-
- println(bounds.String(), "crop", crop.String(), "model", model.String())
- bounds = bounds.Inset(10)
- // bounds =
draw.ApproxBiLinear.Transform(rimg, aff3, img, bounds, draw.Src, nil)
}
diff --git a/internal/render/photo.go b/internal/render/photo.go
index 6bf3913..4769af3 100644
--- a/internal/render/photo.go
+++ b/internal/render/photo.go
@@ -89,7 +89,17 @@ func (photo *Photo) Draw(config *Render, scene *Scene, c *canvas.Context, scales
if config.Sources != nil {
srcs = config.Sources
}
- sources := srcs.EstimateCost(io.Size(size), io.Size(rsize))
+
+ hq := false
+ if config.QualityPreset == QualityPresetHigh {
+ hq = true
+ }
+ costOpts := io.DefaultOptions
+ if hq {
+ costOpts.UnderdrawPenaltyMultiplier = 1000
+ costOpts.DurationCostMultiplier = 0
+ }
+ sources := srcs.EstimateCostWithOpts(io.Size(size), io.Size(rsize), costOpts)
sources.Sort()
var errs []error
@@ -137,7 +147,7 @@ func (photo *Photo) Draw(config *Render, scene *Scene, c *canvas.Context, scales
scale = 0.8
}
- bitmap.DrawImage(config.CanvasImage, img, c, scale)
+ bitmap.DrawImage(config.CanvasImage, img, c, scale, hq)
drawn = true
if source.IsSupportedVideo(path) {
diff --git a/internal/render/rect.go b/internal/render/rect.go
index 22b7338..68bac13 100644
--- a/internal/render/rect.go
+++ b/internal/render/rect.go
@@ -76,6 +76,23 @@ func (rect Rect) FitInside(container Rect) (out Rect) {
return out
}
+func (rect Rect) FillOutside(container Rect) (out Rect) {
+ imageRatio := rect.W / rect.H
+
+ var scale float64
+ if container.W/container.H > imageRatio {
+ scale = container.W / rect.W
+ } else {
+ scale = container.H / rect.H
+ }
+
+ 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 {
return canvas.Identity.
Translate(rect.X, -rect.Y-rect.H)
diff --git a/internal/render/scene.go b/internal/render/scene.go
index 91158a7..f62db03 100644
--- a/internal/render/scene.go
+++ b/internal/render/scene.go
@@ -3,6 +3,7 @@ package render
import (
"image/color"
"math"
+ "runtime"
"sync"
"time"
@@ -14,6 +15,13 @@ import (
"photofield/io"
)
+type QualityPreset int
+
+const (
+ QualityPresetFast QualityPreset = iota
+ QualityPresetHigh
+)
+
type Render struct {
TileSize int `json:"tile_size"`
MaxSolidPixelArea float64 `json:"max_solid_pixel_area"`
@@ -27,6 +35,7 @@ type Render struct {
DebugOverdraw bool
DebugThumbnails bool
+ QualityPreset QualityPreset
Zoom int
CanvasImage draw.Image
@@ -135,7 +144,7 @@ func (scene *Scene) Draw(config *Render, c *canvas.Context, scales Scales, sourc
// photo.Draw(config, scene, c, scales, source)
// }
- concurrent := 10
+ concurrent := runtime.NumCPU()
photoCount := len(scene.Photos)
if photoCount < concurrent {
concurrent = photoCount
@@ -214,7 +223,7 @@ func (scene *Scene) AddPhotosFromIdSlice(ids []image.ImageId) {
}
func (scene *Scene) GetVisiblePhotoRefs(view Rect, maxCount int) <-chan PhotoRef {
- out := make(chan PhotoRef)
+ out := make(chan PhotoRef, 10)
go func() {
count := 0
if maxCount == 0 {
diff --git a/internal/scene/sceneSource.go b/internal/scene/sceneSource.go
index ddcfac6..2394fe7 100644
--- a/internal/scene/sceneSource.go
+++ b/internal/scene/sceneSource.go
@@ -3,6 +3,7 @@ package scene
import (
"fmt"
"log"
+ "strings"
"sync"
"time"
"unsafe"
@@ -82,11 +83,13 @@ func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Sour
finished := metrics.Elapsed("scene load " + config.Collection.Id)
var query *search.Query
+ embFilter := false
if scene.Search != "" {
- searchDone := metrics.Elapsed("search embed")
+ searchDone := metrics.Elapsed("search")
q, err := search.Parse(scene.Search)
if err == nil {
+ embFilter = len(q.QualifierValues("t")) > 0 || len(q.QualifierValues("dedup")) > 0
if similar, err := q.QualifierInt("img"); err == nil {
embedding, err := imageSource.GetImageEmbedding(image.ImageId(similar))
if err != nil {
@@ -94,14 +97,23 @@ func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Sour
scene.Error = fmt.Sprintf("Search failed: %s", err.Error())
}
scene.SearchEmbedding = embedding
- } else if len(q.QualifierValues("tag")) > 0 {
+ query = q
+ } else if len(q.QualifierValues("tag")) > 0 || len(q.QualifierValues("created")) > 0 || embFilter {
query = q
}
+ } else {
+ log.Printf("search parse failed: %s", err.Error())
}
// Fallback
- if scene.SearchEmbedding == nil && scene.Error == "" && query == nil {
- embedding, err := imageSource.Clip.EmbedText(scene.Search)
+ if scene.SearchEmbedding == nil && scene.Error == "" && (query == nil || embFilter) {
+ text := scene.Search
+ if query != nil {
+ text = query.Words()
+ }
+ done := metrics.Elapsed("search embed")
+ embedding, err := imageSource.Clip.EmbedText(text)
+ done()
if err != nil {
log.Println("search embed failed")
scene.Error = fmt.Sprintf("Search failed: %s", err.Error())
@@ -118,7 +130,7 @@ func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Sour
})
layout.LayoutHighlights(infos, config.Layout, &scene, imageSource)
- } else if scene.SearchEmbedding != nil {
+ } else if !embFilter && scene.SearchEmbedding != nil {
// Similarity order
infos := config.Collection.GetSimilar(imageSource, scene.SearchEmbedding, image.ListOptions{
Limit: config.Collection.Limit,
@@ -133,10 +145,16 @@ func (source *SceneSource) loadScene(config SceneConfig, imageSource *image.Sour
}
} else {
// Normal order
+ var extensions []string
+ if strings.Contains(config.Layout.Tweaks, "imageonly") {
+ extensions = imageSource.Images.Extensions
+ }
infos := config.Collection.GetInfos(imageSource, image.ListOptions{
- OrderBy: image.ListOrder(config.Layout.Order),
- Limit: config.Collection.Limit,
- Query: query,
+ OrderBy: image.ListOrder(config.Layout.Order),
+ Limit: config.Collection.Limit,
+ Query: query,
+ Embedding: scene.SearchEmbedding,
+ Extensions: extensions,
})
switch config.Layout.Type {
case layout.Timeline:
@@ -258,6 +276,10 @@ func sceneConfigEqual(a SceneConfig, b SceneConfig) bool {
return false
}
+ if a.Layout.Tweaks != b.Layout.Tweaks {
+ return false
+ }
+
if a.Scene.Search != b.Scene.Search {
return false
}
diff --git a/io/cached/cached.go b/io/cached/cached.go
index 4e5eb85..e21bbfd 100644
--- a/io/cached/cached.go
+++ b/io/cached/cached.go
@@ -56,16 +56,16 @@ func (c *Cached) Exists(ctx context.Context, id io.ImageId, path string) bool {
func (c *Cached) Get(ctx context.Context, id io.ImageId, path string) io.Result {
r := c.Cache.GetWithName(ctx, id, c.Source.Name())
- // fmt.Printf("%v %v\n", r.Image, r.Error)
+ // fmt.Printf("%v %v %v\n", id, c.Source.Name(), r.Error)
if r.Image != nil || r.Error != nil {
- // fmt.Printf("%v cache found\n", id)
+ // fmt.Printf("%v %v cache found\n", id, c.Source.Name())
// println("found in cache")
r.FromCache = true
return r
}
// r = c.Source.Get(ctx, id, path)
r = c.load(ctx, id, path)
- // fmt.Printf("%v cache load end\n", id)
+ // fmt.Printf("%v %v cache load end\n", id, c.Source.Name())
// c.Ristretto.SetWithName(ctx, id, c.Source.Name(), r)
// fmt.Printf("%v cache set\n", id)
// println("saved to cache", s)
diff --git a/io/io.go b/io/io.go
index 2196248..2f9b999 100644
--- a/io/io.go
+++ b/io/io.go
@@ -141,11 +141,19 @@ type Sources []Source
// var DurationCostMultiplier = 0.003
// Optimized for 0.9 max width ratio + square duration
-var UnderdrawPenaltyMultiplier = 59.851585
-var SizeCostMultiplier = 0.000281
-var DurationCostMultiplier = 0.011857
+var DefaultOptions = Options{
+ UnderdrawPenaltyMultiplier: 59.851585,
+ SizeCostMultiplier: 0.000281,
+ DurationCostMultiplier: 0.011857,
+}
+
+type Options struct {
+ UnderdrawPenaltyMultiplier float64
+ SizeCostMultiplier float64
+ DurationCostMultiplier float64
+}
-func SizeCost(source Size, original Size, target Size) (cost float64, area int64) {
+func SizeCost(source Size, original Size, target Size, opts Options) (cost float64, area int64) {
if source.X == 0 && source.Y == 0 {
source = target
}
@@ -153,24 +161,24 @@ func SizeCost(source Size, original Size, target Size) (cost float64, area int64
targetArea := target.Area()
diff := float64(targetArea) - float64(area)
if targetArea > area {
- diff *= UnderdrawPenaltyMultiplier
+ diff *= opts.UnderdrawPenaltyMultiplier
}
- cost = diff * diff * SizeCostMultiplier
+ cost = diff * diff * opts.SizeCostMultiplier
return
}
-func DurationCost(dur time.Duration) float64 {
+func DurationCost(dur time.Duration, opts Options) float64 {
us := float64(dur.Microseconds())
- return us * us * DurationCostMultiplier
+ return us * us * opts.DurationCostMultiplier
}
-func (sources Sources) EstimateCost(original Size, target Size) SourceCosts {
+func (sources Sources) EstimateCostWithOpts(original Size, target Size, opts Options) SourceCosts {
costs := make([]SourceCost, len(sources))
for i := range sources {
s := sources[i]
- sizecost, sarea := SizeCost(s.Size(original), original, target)
+ sizecost, sarea := SizeCost(s.Size(original), original, target, opts)
dur := s.GetDurationEstimate(original)
- durcost := DurationCost(dur)
+ durcost := DurationCost(dur, opts)
cost := sizecost + durcost
costs[i] = SourceCost{
Source: s,
@@ -184,6 +192,10 @@ func (sources Sources) EstimateCost(original Size, target Size) SourceCosts {
return costs
}
+func (sources Sources) EstimateCost(original Size, target Size) SourceCosts {
+ return sources.EstimateCostWithOpts(original, target, DefaultOptions)
+}
+
func (sources Sources) Close() {
for _, s := range sources {
err := s.Close()
diff --git a/io/ristretto/ristretto.go b/io/ristretto/ristretto.go
index 574353b..4cfb2fd 100644
--- a/io/ristretto/ristretto.go
+++ b/io/ristretto/ristretto.go
@@ -36,13 +36,11 @@ func (ids IdWithSize) String() string {
return fmt.Sprintf("%6d %4d %4d", ids.Id, ids.Size.X, ids.Size.Y)
}
-func New() *Ristretto {
- maxSizeBytes := int64(256000000)
-
+func New(sizeBytes int64) *Ristretto {
cache, err := drist.NewCache(&drist.Config[IdWithName, io.Result]{
- NumCounters: 1e6, // number of keys to track frequency of
- MaxCost: maxSizeBytes, // maximum cost of cache
- BufferItems: 64, // number of keys per Get buffer
+ NumCounters: 1e6, // number of keys to track frequency of
+ MaxCost: sizeBytes, // maximum cost of cache
+ BufferItems: 64, // number of keys per Get buffer
Metrics: true,
Cost: cost,
KeyToHash: keyToHash,
@@ -117,11 +115,20 @@ func (r *Ristretto) Set(ctx context.Context, id io.ImageId, path string, v io.Re
return r.cache.SetWithTTL(idn, v, 0, 10*time.Minute)
}
+func printedCost(r io.Result) int64 {
+ c := cost(r)
+ if r.Image != nil {
+ fmt.Printf("cost %v %v %v\n", r.Image.Bounds().Dx(), r.Image.Bounds().Dy(), c/1000000)
+ }
+ return c
+}
+
func cost(r io.Result) int64 {
img := r.Image
if img == nil {
return 1
}
+
switch img := img.(type) {
case *image.YCbCr:
diff --git a/justfile b/justfile
index 259d602..67b40e7 100644
--- a/justfile
+++ b/justfile
@@ -82,20 +82,17 @@ grafana-export:
pprof := "http://localhost:8080/debug/pprof"
prof-cpu seconds="10":
- mkdir -p profiles/cpu/
- filepath=profiles/cpu/cpu-$(date +"%F-%H%M%S").pprof && \
- curl --progress-bar -o $filepath {{pprof}}/profile?seconds={{seconds}} && \
- go tool pprof -http=: $filepath
+ go tool pprof -http=: {{pprof}}/profile?seconds={{seconds}}
prof-heap:
- mkdir -p profiles/heap/
- filepath=profiles/heap/heap-$(date +"%F-%H%M%S").pprof && \
- curl --progress-bar -o $filepath {{pprof}}/heap && \
- go tool pprof -http=: $filepath
+ go tool pprof -http=: {{pprof}}/heap
prof-reload:
go test -benchmem -benchtime 10s '-run=^$' -bench '^BenchmarkReload$' photofield
+monitor:
+ docker compose up prometheus grafana pyroscope
+
test-reload:
mkdir -p profiles/
go test -v '-run=^TestReloadLeaks$' photofield
diff --git a/main.go b/main.go
index 6d1b925..858d37b 100644
--- a/main.go
+++ b/main.go
@@ -386,6 +386,9 @@ func (*Api) PostScenes(w http.ResponseWriter, r *http.Request) {
sceneConfig.Layout.ViewportWidth = float64(data.ViewportWidth)
sceneConfig.Layout.ViewportHeight = float64(data.ViewportHeight)
+ if data.Tweaks != nil {
+ sceneConfig.Layout.Tweaks = string(*data.Tweaks)
+ }
sceneConfig.Layout.ImageHeight = 0
if data.ImageHeight != nil {
sceneConfig.Layout.ImageHeight = float64(*data.ImageHeight)
@@ -437,6 +440,9 @@ func (*Api) GetScenes(w http.ResponseWriter, r *http.Request, params openapi.Get
if params.Search != nil {
sceneConfig.Scene.Search = string(*params.Search)
}
+ if params.Tweaks != nil {
+ sceneConfig.Layout.Tweaks = string(*params.Tweaks)
+ }
collection := getCollectionById(string(params.CollectionId))
if collection == nil {
problem(w, r, http.StatusBadRequest, "Collection not found")
@@ -470,10 +476,7 @@ func (*Api) GetScenesId(w http.ResponseWriter, r *http.Request, id openapi.Scene
}
func (*Api) GetCollections(w http.ResponseWriter, r *http.Request) {
- for i := range collections {
- collection := &collections[i]
- collection.UpdateStatus(imageSource)
- }
+ // Explicitly do not update collection status to avoid long delays
items := collections
if items == nil {
items = make([]collection.Collection, 0)
@@ -742,6 +745,14 @@ func GetScenesSceneIdTilesImpl(w http.ResponseWriter, r *http.Request, sceneId o
if params.DebugThumbnails != nil {
rn.DebugThumbnails = *params.DebugThumbnails
}
+ if params.QualityPreset != nil {
+ switch *params.QualityPreset {
+ case "HIGH":
+ rn.QualityPreset = render.QualityPresetHigh
+ default:
+ rn.QualityPreset = render.QualityPresetFast
+ }
+ }
zoom := params.Zoom
x := int(params.X)
@@ -786,7 +797,11 @@ func GetScenesSceneIdTilesImpl(w http.ResponseWriter, r *http.Request, sceneId o
return
}
w.Header().Add("Content-Type", "image/jpeg")
- codec.EncodeJpeg(w, img)
+ quality := 80
+ if rn.QualityPreset == render.QualityPresetHigh {
+ quality = 100
+ }
+ codec.EncodeJpeg(w, img, quality)
}
func (*Api) GetScenesSceneIdDates(w http.ResponseWriter, r *http.Request, sceneId openapi.SceneId, params openapi.GetScenesSceneIdDatesParams) {
diff --git a/search/query.go b/search/query.go
index 1464574..d8b3c3f 100644
--- a/search/query.go
+++ b/search/query.go
@@ -2,7 +2,10 @@ package search
import (
"fmt"
+ "os"
"strconv"
+ "strings"
+ "time"
"github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer"
@@ -13,9 +16,10 @@ type Query struct {
}
type Term struct {
- String *string `parser:"@String" json:"string,omitempty"`
+ Not bool `parser:"@'NOT'?" json:"not,omitempty"`
+ String *string `parser:"(@String" json:"string,omitempty"`
Qualifier *Qualifier `parser:"| @@" json:"qualifier,omitempty"`
- Word *string `parser:"| @Word" json:"word,omitempty"`
+ Word *string `parser:"| @Word)" json:"word,omitempty"`
Pos lexer.Position `parser:"" json:"start"`
EndPos lexer.Position `parser:"" json:"end"`
}
@@ -28,47 +32,133 @@ type Qualifier struct {
var lex *lexer.StatefulDefinition
var par *participle.Parser[Query]
+var ErrNilQuery = fmt.Errorf("nil query")
+var ErrNotFound = fmt.Errorf("not found")
+
func init() {
lex = lexer.MustSimple([]lexer.SimpleRule{
- {Name: "Whitespace", Pattern: `[ \t]+`},
- {Name: "Word", Pattern: `[^\s:]+`},
{Name: "String", Pattern: `"(\\"|[^"])*"`},
+ {Name: "Word", Pattern: `[^\s:]+`},
{Name: "Colon", Pattern: `:`},
+ {Name: "Whitespace", Pattern: `[ \t]+`},
})
-
par = participle.MustBuild[Query](
participle.Lexer(lex),
participle.Elide("Whitespace"),
participle.Unquote("String"),
+ participle.UseLookahead(2),
)
}
+func PrintTokens(str string) {
+ l, err := lex.LexString("", str)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ for {
+ tok, err := l.Next()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ if tok.EOF() {
+ break
+ }
+ fmt.Println(tok.GoString())
+ }
+}
+
func Parse(str string) (*Query, error) {
return par.ParseString("", str)
}
+func ParseDebug(str string) (*Query, error) {
+ PrintTokens(str)
+ return par.ParseString("", str, participle.Trace(os.Stdout))
+}
+
func (q *Query) QualifierInt(key string) (int, error) {
if q == nil {
- return 0, fmt.Errorf("nil query")
+ return 0, ErrNilQuery
}
- if len(q.Terms) == 0 {
- return 0, fmt.Errorf("empty query")
+ values := q.QualifierValues(key)
+ if len(values) == 0 {
+ return 0, ErrNotFound
}
- if len(q.Terms) > 1 {
- return 0, fmt.Errorf("too many terms")
+ if len(values) > 1 {
+ return 0, fmt.Errorf("multiple qualifiers %s", key)
}
- if q.Terms[0].Qualifier == nil {
- return 0, fmt.Errorf("no qualifier")
+ return strconv.Atoi(q.Terms[0].Qualifier.Value)
+}
+
+func (q *Query) QualifierFloat32(key string) (float32, error) {
+ if q == nil {
+ return 0, ErrNilQuery
}
- if q.Terms[0].Qualifier.Key != key {
- return 0, fmt.Errorf(`qualifier not %s`, key)
+ values := q.QualifierValues(key)
+ if len(values) == 0 {
+ return 0, ErrNotFound
}
- return strconv.Atoi(q.Terms[0].Qualifier.Value)
+ if len(values) > 1 {
+ return 0, fmt.Errorf("multiple qualifiers %s", key)
+ }
+
+ value := values[0]
+
+ f, err := strconv.ParseFloat(value, 32)
+ if err != nil {
+ return 0, fmt.Errorf("failed to parse float32: %v", err)
+ }
+
+ return float32(f), nil
+}
+
+func (q *Query) QualifierDateRange(key string) (a time.Time, b time.Time, err error) {
+ if q == nil {
+ err = ErrNilQuery
+ return
+ }
+
+ values := q.QualifierValues(key)
+ if len(values) == 0 {
+ err = ErrNotFound
+ return
+ }
+
+ if len(values) > 1 {
+ err = fmt.Errorf("multiple qualifiers %s", key)
+ return
+ }
+
+ value := values[0]
+
+ dateRange := strings.SplitN(value, "..", 2)
+ if len(dateRange) != 2 {
+ err = fmt.Errorf("invalid date range format")
+ return
+ }
+
+ a, err = time.Parse("2006-01-02", dateRange[0])
+ if err != nil {
+ err = fmt.Errorf("failed to parse start date: %v", err)
+ return
+ }
+
+ b, err = time.Parse("2006-01-02", dateRange[1])
+ if err != nil {
+ err = fmt.Errorf("failed to parse end date: %v", err)
+ return
+ }
+
+ b = b.AddDate(0, 0, 1)
+
+ return
}
func (q *Query) QualifierValues(key string) []string {
@@ -83,3 +173,19 @@ func (q *Query) QualifierValues(key string) []string {
}
return values
}
+
+func (q *Query) Words() string {
+ if q == nil {
+ return ""
+ }
+ var words string
+ for _, term := range q.Terms {
+ if term.Word != nil {
+ words += *term.Word + " "
+ }
+ }
+ if len(words) == 0 {
+ return ""
+ }
+ return words[:len(words)-1]
+}
diff --git a/search/query_test.go b/search/query_test.go
index d23520c..b62c001 100644
--- a/search/query_test.go
+++ b/search/query_test.go
@@ -2,6 +2,7 @@ package search
import (
"testing"
+ "time"
"github.com/alecthomas/assert/v2"
)
@@ -52,3 +53,82 @@ func TestQualifierValues(t *testing.T) {
query.QualifierValues("tag"),
)
}
+
+func TestWords(t *testing.T) {
+ query, err := Parse("hello world created:2016-04-29..2016-07-04")
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(
+ t,
+ []string{"2016-04-29..2016-07-04"},
+ query.QualifierValues("created"),
+ )
+ assert.Equal(t, "hello world", query.Words())
+}
+
+func TestEmptyWords(t *testing.T) {
+ query, err := Parse("tag:hello")
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(t, "", query.Words())
+}
+func TestQualifierDateRange(t *testing.T) {
+ query, err := Parse("created:2022-01-01..2022-12-31")
+ if err != nil {
+ t.Error(err)
+ }
+
+ startDate, endDate, err := query.QualifierDateRange("created")
+ if err != nil {
+ t.Error(err)
+ }
+
+ expectedStartDate, _ := time.Parse("2006-01-02", "2022-01-01")
+ expectedEndDate, _ := time.Parse("2006-01-02", "2023-01-01")
+
+ if !startDate.Equal(expectedStartDate) {
+ t.Errorf("Expected start date '%s', got '%s'", expectedStartDate.Format("2006-01-02"), startDate.Format("2006-01-02"))
+ }
+
+ if !endDate.Equal(expectedEndDate) {
+ t.Errorf("Expected end date '%s', got '%s'", expectedEndDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
+ }
+}
+
+func TestQualifierDateRangeNoQualifier(t *testing.T) {
+ query, err := Parse("hello world")
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, _, err = query.QualifierDateRange("created")
+ if err == nil {
+ t.Error("Expected error, got nil")
+ }
+}
+
+func TestQualifierDateRangeMultipleQualifiers(t *testing.T) {
+ query, err := Parse("created:2022-01-01..2022-12-31 created:2023-01-01..2023-12-31")
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, _, err = query.QualifierDateRange("created")
+ if err == nil {
+ t.Error("Expected error, got nil")
+ }
+}
+
+func TestQualifierDateRangeInvalidDateFormat(t *testing.T) {
+ query, err := Parse("created:2022-01-01..2022-12-31")
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, _, err = query.QualifierDateRange("invalid")
+ if err == nil {
+ t.Error("Expected error, got nil")
+ }
+}
diff --git a/ui/src/api.js b/ui/src/api.js
index a13bcf3..cdcc1dd 100644
--- a/ui/src/api.js
+++ b/ui/src/api.js
@@ -184,6 +184,7 @@ export function useScene({
imageHeight,
viewport,
search,
+ tweaks,
}) {
const sceneParams = computed(() =>
@@ -197,6 +198,7 @@ export function useScene({
viewport_width: viewport.width.value,
viewport_height: viewport.height.value,
search: search?.value || undefined,
+ tweaks: tweaks.value,
}
);
diff --git a/ui/src/components/CollectionView.vue b/ui/src/components/CollectionView.vue
index e17f714..02550d0 100644
--- a/ui/src/components/CollectionView.vue
+++ b/ui/src/components/CollectionView.vue
@@ -38,6 +38,7 @@
:imageHeight="imageHeight"
:search="search"
:debug="debug"
+ :tweaks="tweaks"
:fullpage="true"
:scrollbar="scrollbar"
:selectTagId="selectTagId"
@@ -227,6 +228,10 @@ const debug = computed(() => {
return v;
});
+const tweaks = computed(() => {
+ return route.query.tweaks;
+});
+
const onRegion = async (region) => {
if (!region) return;
const r = {
diff --git a/ui/src/components/MapViewer.vue b/ui/src/components/MapViewer.vue
index 1336380..083e937 100644
--- a/ui/src/components/MapViewer.vue
+++ b/ui/src/components/MapViewer.vue
@@ -79,6 +79,7 @@ const props = defineProps({
debug: Object,
fullpage: Boolean,
scrollbar: Object,
+ tweaks: String,
});
const emit = defineEmits({
@@ -103,6 +104,7 @@ const {
search,
selectTagId,
debug,
+ tweaks,
} = toRefs(props);
const viewer = ref(null);
@@ -124,6 +126,7 @@ const { scene, recreate: recreateScene, loadSpeed } = useScene({
imageHeight,
viewport: staticViewport,
search,
+ tweaks,
});
const {
diff --git a/ui/src/components/ScrollViewer.vue b/ui/src/components/ScrollViewer.vue
index f001611..12e33eb 100644
--- a/ui/src/components/ScrollViewer.vue
+++ b/ui/src/components/ScrollViewer.vue
@@ -17,6 +17,7 @@
:focus="!!region"
:crossNav="!!region"
:viewport="viewport"
+ :qualityPreset="qualityPreset"
@click="onClick"
@view="onView"
@nav="onNav"
@@ -92,6 +93,7 @@ const props = defineProps({
debug: Object,
fullpage: Boolean,
scrollbar: Object,
+ tweaks: String,
});
const emit = defineEmits({
@@ -118,6 +120,7 @@ const {
search,
selectTagId,
debug,
+ tweaks,
} = toRefs(props);
const viewer = ref(null);
@@ -133,6 +136,12 @@ const { scene, recreate: recreateScene, loadSpeed } = useScene({
imageHeight,
viewport,
search,
+ tweaks,
+});
+
+const qualityPreset = computed(() => {
+ if (tweaks.value?.indexOf("hq") > -1) return "HIGH";
+ return null;
});
watch(scene, async (newScene, oldScene) => {
diff --git a/ui/src/components/TileViewer.vue b/ui/src/components/TileViewer.vue
index 289af00..5e9ea92 100644
--- a/ui/src/components/TileViewer.vue
+++ b/ui/src/components/TileViewer.vue
@@ -79,6 +79,7 @@ export default {
loading: Boolean,
geo: Boolean,
viewport: Object,
+ qualityPreset: String,
},
emits: [
@@ -721,6 +722,9 @@ export default {
if (this.selectTagId) {
extra.select_tag = this.selectTagId;
}
+ if (this.qualityPreset) {
+ extra.quality_preset = this.qualityPreset;
+ }
return getTileUrl(
this.scene.id,
z, x, y,