diff --git a/.env.dev b/.env.dev index bcc04d74..f5a55ec6 100644 --- a/.env.dev +++ b/.env.dev @@ -11,5 +11,5 @@ export PROCESSED_DIRECTORY=$(pwd)/dev_root/dev_processed/ export STEAMGRIDDB_API_KEY="" export ADMIN_PASSWORD=admin export ADMIN_USERNAME=admin -export DEMO_MODE=true +export DEMO_MODE=false export DEMO_UPLOAD_LIMIT_MB=500 diff --git a/Dockerfile b/Dockerfile index c101d01c..8543ca85 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM node:24-slim as client +FROM node:24-slim AS client WORKDIR /app -ENV PATH /app/node_modules/.bin:$PATH +ENV PATH=/app/node_modules/.bin:$PATH COPY app/client/package.json ./ COPY app/client/package-lock.json ./ COPY app/client/.env.* ./ @@ -9,10 +9,14 @@ COPY app/client/ ./ RUN npm run build # FFmpeg builder stage with CUDA development tools -FROM nvidia/cuda:11.8.0-devel-ubuntu22.04 as ffmpeg-builder +FROM nvidia/cuda:11.8.0-devel-ubuntu22.04 AS ffmpeg-builder WORKDIR /tmp # Install build dependencies +# Remove stale NVIDIA apt sources (GPG keys rotate) and swap archive.ubuntu.com +# for kernel.org's mirror, which is accessible when Canonical's servers aren't +RUN rm -f /etc/apt/sources.list.d/cuda.list /etc/apt/sources.list.d/nvidia-ml.list \ + && sed -i 's|http://archive.ubuntu.com/ubuntu|http://mirrors.edge.kernel.org/ubuntu|g' /etc/apt/sources.list RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ cmake \ @@ -155,18 +159,18 @@ RUN python3.14 -m pip install --no-cache-dir --break-system-packages --ignore-in && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* /root/.cache/pip /tmp/* -ENV FLASK_APP /app/server/fireshare:create_app() -ENV ENVIRONMENT production -ENV DATA_DIRECTORY /data -ENV VIDEO_DIRECTORY /videos -ENV IMAGE_DIRECTORY /images -ENV PROCESSED_DIRECTORY /processed +ENV FLASK_APP=/app/server/fireshare:create_app() +ENV ENVIRONMENT=production +ENV DATA_DIRECTORY=/data +ENV VIDEO_DIRECTORY=/videos +ENV IMAGE_DIRECTORY=/images +ENV PROCESSED_DIRECTORY=/processed ENV TEMPLATE_PATH=/app/server/fireshare/templates -ENV ADMIN_PASSWORD admin -ENV ANALYTICS_TRACKING_SCRIPT "" +ENV ADMIN_PASSWORD=admin +ENV ANALYTICS_TRACKING_SCRIPT="" ENV TZ=UTC -ENV LD_LIBRARY_PATH /usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/lib:/usr/local/cuda/lib64:$LD_LIBRARY_PATH -ENV PATH /usr/local/bin:$PATH +ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/lib:/usr/local/cuda/lib64:$LD_LIBRARY_PATH +ENV PATH=/usr/local/bin:$PATH EXPOSE 80 CMD ["bash", "/entrypoint.sh"] diff --git a/app/client/package-lock.json b/app/client/package-lock.json index 7b188f88..38bdf97b 100644 --- a/app/client/package-lock.json +++ b/app/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "fireshare", - "version": "1.5.7", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fireshare", - "version": "1.5.7", + "version": "1.5.0", "dependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", @@ -26,10 +26,12 @@ "react-dom": "^18.1.0", "react-helmet": "^6.1.0", "react-is-visible": "^1.2.0", + "react-markdown": "^10.1.0", "react-player": "^2.10.1", "react-router-dom": "^6.3.0", "react-select": "^5.3.2", "react-zoom-pan-pinch": "^4.0.3", + "remark-gfm": "^4.0.1", "typeface-roboto": "^1.1.13", "wavesurfer.js": "^7.12.4" }, @@ -1684,11 +1686,52 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/parse-json": { @@ -1722,6 +1765,18 @@ "@types/react": "*" } }, + "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==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@videojs/core": { "version": "10.0.0-beta.4", "resolved": "https://registry.npmjs.org/@videojs/core/-/core-10.0.0-beta.4.tgz", @@ -1847,6 +1902,16 @@ "npm": ">=6" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/browserslist": { "version": "4.25.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", @@ -1923,6 +1988,56 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1944,6 +2059,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2023,6 +2148,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2041,6 +2179,28 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2190,6 +2350,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-average-color": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-9.5.0.tgz", @@ -2390,6 +2566,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hls.js": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", @@ -2411,6 +2627,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2427,6 +2653,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2448,6 +2704,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2509,6 +2797,16 @@ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2531,6 +2829,16 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", @@ -2546,105 +2854,975 @@ "node": ">= 0.4" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/motion-dom": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", - "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { - "motion-utils": "^12.29.2" + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/motion-utils": { - "version": "12.29.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", - "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/motion-dom": { + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", + "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -2726,6 +3904,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2858,6 +4046,33 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-player": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.1.tgz", @@ -2981,6 +4196,72 @@ "lodash": "^4.0.1" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3038,6 +4319,48 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -3116,6 +4439,26 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3154,6 +4497,93 @@ "node": "*" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3199,6 +4629,34 @@ } } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -3362,6 +4820,16 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/app/client/package.json b/app/client/package.json index a13696c0..d7e9ede7 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -1,6 +1,6 @@ { "name": "fireshare", - "version": "1.6.1", + "version": "1.6.2", "private": true, "dependencies": { "@emotion/react": "^11.9.0", @@ -21,10 +21,12 @@ "react-dom": "^18.1.0", "react-helmet": "^6.1.0", "react-is-visible": "^1.2.0", + "react-markdown": "^10.1.0", "react-player": "^2.10.1", "react-router-dom": "^6.3.0", "react-select": "^5.3.2", "react-zoom-pan-pinch": "^4.0.3", + "remark-gfm": "^4.0.1", "typeface-roboto": "^1.1.13", "wavesurfer.js": "^7.12.4" }, diff --git a/app/client/src/components/modal/ReleaseNotesDialog.js b/app/client/src/components/modal/ReleaseNotesDialog.js new file mode 100644 index 00000000..e5d7847e --- /dev/null +++ b/app/client/src/components/modal/ReleaseNotesDialog.js @@ -0,0 +1,368 @@ +import * as React from 'react' +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Typography, + Button, + Divider, + CircularProgress, + IconButton, +} from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' +import OpenInNewIcon from '@mui/icons-material/OpenInNew' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { ReleaseService } from '../../services' +import { dialogPaperSx } from '../../common/modalStyles' + +const INITIAL_LIMIT = 3 + +function formatDate(isoString) { + if (!isoString) return '' + return new Date(isoString).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) +} + +// Custom renderers for GitHub-flavored markdown, dark theme +const markdownComponents = { + h1: ({ children }) => ( + + {children} + + ), + h2: ({ children }) => ( + + {children} + + ), + h3: ({ children }) => ( + + {children} + + ), + p: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( + + {children} + + ), + ol: ({ children }) => ( + + {children} + + ), + li: ({ children }) => ( + + {children} + + ), + a: ({ href, children }) => ( + + {children} + + ), + strong: ({ children }) => ( + + {children} + + ), + em: ({ children }) => ( + + {children} + + ), + blockquote: ({ children }) => ( + + {children} + + ), + hr: () => , + pre: ({ children }) => ( + + {children} + + ), + code: ({ className, children }) => { + // Fenced code blocks have a language-xxx className; inline code does not + if (className) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) + }, +} + +function ReleaseCard({ release, showDivider, isLatest }) { + const showName = release.name && release.name !== `v${release.version}` && release.name !== release.version + + return ( + + {/* Header row */} + + + v{release.version} + + {showName && ( + + {release.name} + + )} + + {formatDate(release.published_at)} + + + + + {/* Markdown body */} + *:first-of-type': { mt: '0 !important' } }}> + + {release.body || '_No release notes provided._'} + + + + {/* GitHub link */} + {release.html_url && ( + + View on GitHub + + + )} + + {showDivider && } + + ) +} + +function ReleaseNotesDialog({ open, onClose, authenticated }) { + const [releases, setReleases] = React.useState([]) + const [loading, setLoading] = React.useState(false) + const [hasMore, setHasMore] = React.useState(false) + const [offset, setOffset] = React.useState(0) + + const fetchReleases = React.useCallback(async (currentOffset) => { + setLoading(true) + try { + const res = await ReleaseService.getReleases(currentOffset, INITIAL_LIMIT) + const data = res.data + setReleases((prev) => (currentOffset === 0 ? data.releases : [...prev, ...data.releases])) + setHasMore(data.has_more) + setOffset(currentOffset + data.releases.length) + } catch (err) { + console.error('Failed to fetch releases', err) + } + setLoading(false) + }, []) + + React.useEffect(() => { + if (open) { + setReleases([]) + setOffset(0) + setHasMore(false) + fetchReleases(0) + } + }, [open, fetchReleases]) + + const handleClose = () => { + if (releases.length > 0 && authenticated) { + ReleaseService.setLastSeenVersion(releases[0].version).catch(() => {}) + } + onClose() + } + + return ( + + + + + Release Notes + + + + + + + + + {loading && releases.length === 0 ? ( + + + + ) : ( + <> + {releases.map((release, i) => ( + + ))} + + {loading ? ( + + ) : hasMore ? ( + + ) : null} + + + )} + + + + + + + ) +} + +export default ReleaseNotesDialog diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js index 065f1e63..db1a97a1 100644 --- a/app/client/src/components/nav/Navbar20.js +++ b/app/client/src/components/nav/Navbar20.js @@ -50,6 +50,7 @@ import { RegisterUploadCardContext, RegisterImageUploadCardContext } from '../ut import Select from 'react-select' import selectFolderTheme from '../../common/reactSelectFolderTheme' import VersionBox from './VersionBox' +import ReleaseNotesDialog from '../modal/ReleaseNotesDialog' const drawerWidth = 240 const minimizedDrawerWidth = 57 @@ -144,8 +145,7 @@ const AppBar = styled(MuiAppBar, { function Navbar20({ authenticated, isAdmin, - showReleaseNotes, - releaseNotes, + latestRelease, page, collapsed = false, searchable = false, @@ -163,6 +163,30 @@ function Navbar20({ const [open, setOpen] = React.useState(!collapsed) const [cardSize, setCardSize] = React.useState(getSetting('cardSize') || CARD_SIZE_DEFAULT) + const [featureAlertOpen, setFeatureAlertOpen] = React.useState(false) + + // Auto-open release notes on first visit when a new version is available + React.useEffect(() => { + if (!latestRelease?.version) return + const cookieName = 'release_notes_seen_version' + const seenVersion = document.cookie + .split('; ') + .find((c) => c.startsWith(cookieName + '=')) + ?.split('=')[1] + if (seenVersion !== latestRelease.version) { + setFeatureAlertOpen(true) + } + }, [latestRelease]) + + const handleReleaseNotesClose = React.useCallback(() => { + if (latestRelease?.version) { + const expires = new Date() + expires.setFullYear(expires.getFullYear() + 1) + document.cookie = `release_notes_seen_version=${latestRelease.version}; expires=${expires.toUTCString()}; path=/; SameSite=Lax` + } + setFeatureAlertOpen(false) + }, [latestRelease]) + const [alert, setAlert] = React.useState({ open: false }) const [uploadTick, setUploadTick] = React.useState(0) const registerUploadCard = React.useContext(RegisterUploadCardContext) @@ -251,7 +275,7 @@ function Navbar20({ } } - const handleCardSizeChange = (e, newValue) => { + const handleCardSizeChange = (_e, newValue) => { const newSize = Math.round((newValue / 100) * CARD_SIZE_DEFAULT * CARD_SIZE_MULTIPLIER) setCardSize(newSize) setSetting('cardSize', newSize) @@ -627,7 +651,11 @@ function Navbar20({ )} - + setFeatureAlertOpen(true)} + /> ) @@ -844,8 +872,6 @@ function Navbar20({ isAdmin, searchText, cardSize, - showReleaseNotes, - releaseNotes, selectedFolder: effectiveFolder, onFolderChange: handleFolderChange, onFoldersLoaded: handleFoldersLoaded, @@ -855,6 +881,11 @@ function Navbar20({ uploadTick, })} + ) } diff --git a/app/client/src/components/nav/VersionBox.js b/app/client/src/components/nav/VersionBox.js index 99f0e64d..fb4739c6 100644 --- a/app/client/src/components/nav/VersionBox.js +++ b/app/client/src/components/nav/VersionBox.js @@ -4,6 +4,7 @@ import LightTooltip from '../misc/LightTooltip' import GitHubIcon from '@mui/icons-material/GitHub' import VolunteerActivismIcon from '@mui/icons-material/VolunteerActivism' import BugReportIcon from '@mui/icons-material/BugReport' +import NewReleasesIcon from '@mui/icons-material/NewReleases' const GITHUB_URL = 'https://github.com/ShaneIsrael/fireshare' const ISSUES_URL = 'https://github.com/ShaneIsrael/fireshare/issues' @@ -19,32 +20,63 @@ const actionIconSx = { }, } -function VersionBox({ open }) { +function VersionBox({ open, releaseNotes, onUpdateClick }) { + const hasUpdate = Boolean(releaseNotes) + if (!open) return ( - - window.open(GITHUB_URL, '_blank')} - > - - - + + + window.open(GITHUB_URL, '_blank')} + > + + + + {hasUpdate && ( + + + + + + )} + ) return ( @@ -97,6 +129,22 @@ function VersionBox({ open }) { {/* Right: action icons */} + {hasUpdate && ( + + + + + + )} { const [authed, setAuthed] = React.useState(true) const [checkingAuth, setCheckingAuth] = React.useState(true) const [isAdmin, setIsAdmin] = React.useState(false) - const [showReleaseNotes, setShowReleaseNotes] = React.useState(false) - const [releaseNotes, setReleaseNotes] = React.useState(null) + const [latestRelease, setLatestRelease] = React.useState(null) const checkLogin = React.useCallback(async () => { try { @@ -18,8 +17,7 @@ const AuthWrapper = ({ children, redirect }) => { if (typeof response === 'object') { setAuthed(response.authenticated) setIsAdmin(response.admin || false) - setShowReleaseNotes(response.show_release_notes || false) - setReleaseNotes(response.release_notes || null) + setLatestRelease(response.latest_release || null) } else { setAuthed(response) } @@ -53,9 +51,7 @@ const AuthWrapper = ({ children, redirect }) => { const childProps = { authenticated: authed, isAdmin, - showReleaseNotes, - releaseNotes, - setShowReleaseNotes, + latestRelease, } if (!redirect) return React.cloneElement(children, childProps) diff --git a/app/client/src/services/ReleaseService.js b/app/client/src/services/ReleaseService.js index afcdf5de..b7ce1f1a 100644 --- a/app/client/src/services/ReleaseService.js +++ b/app/client/src/services/ReleaseService.js @@ -4,6 +4,9 @@ const service = { getReleaseNotes() { return Api().get('/api/release-notes') }, + getReleases(offset = 0, limit = 3) { + return Api().get('/api/releases', { params: { offset, limit } }) + }, setLastSeenVersion(version) { return Api().put('/api/user/last-seen-version', { version }) }, diff --git a/app/client/src/views/Dashboard.js b/app/client/src/views/Dashboard.js index 6f9a01d4..b7e262ad 100644 --- a/app/client/src/views/Dashboard.js +++ b/app/client/src/views/Dashboard.js @@ -25,7 +25,7 @@ import LinkIcon from '@mui/icons-material/Link' import LocalOfferIcon from '@mui/icons-material/LocalOffer' import VideoCards from '../components/cards/VideoCards' import LoadingSpinner from '../components/misc/LoadingSpinner' -import { VideoService, GameService, ReleaseService, TagService } from '../services' +import { VideoService, GameService, TagService } from '../services' import Select from 'react-select' import SnackbarAlert from '../components/alert/SnackbarAlert' import TagChip from '../components/misc/TagChip' @@ -38,8 +38,6 @@ const Dashboard = ({ authenticated, searchText, cardSize, - showReleaseNotes, - releaseNotes: releaseNotesProp, selectedFolder, onFoldersLoaded, uploadTick, @@ -70,8 +68,6 @@ const Dashboard = ({ const [allTags, setAllTags] = React.useState([]) const [selectedTagsForBulk, setSelectedTagsForBulk] = React.useState([]) const [tagInputValueBulk, setTagInputValueBulk] = React.useState('') - const [featureAlertOpen, setFeatureAlertOpen] = React.useState(showReleaseNotes) - const releaseNotes = releaseNotesProp const [toolbarTarget, setToolbarTarget] = React.useState(null) const theme = useTheme() const isMdDown = useMediaQuery(theme.breakpoints.down('md')) @@ -182,12 +178,6 @@ const Dashboard = ({ } }, [editMode, isMdDown]) - const handleFeatureAlertClose = () => { - if (releaseNotes?.version && authenticated) { - ReleaseService.setLastSeenVersion(releaseNotes.version).catch(() => {}) - } - setFeatureAlertOpen(false) - } // Use folder from Navbar props (falls back to All Videos) const folder = selectedFolder || { value: 'All Videos', label: 'All Videos' } @@ -736,68 +726,6 @@ const Dashboard = ({ - {/* Release Notes Dialog */} - - {`New Update Available - v${releaseNotes?.version}`} - - /g, '>') - // Headers - .replace(/^## (.+)$/gm, '$1') - .replace(/^### (.+)$/gm, '$1') - // Bold - .replace(/\*\*(.+?)\*\*/g, '$1') - // Links - .replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - '$1', - ) - .replace(/(https?:\/\/[^\s<]+)/g, '$1') - // Unordered lists - .replace(/(?:^|\n)([*\-] .+(?:\n[*\-] .+)*)/g, (match) => { - const items = match - .trim() - .split('\n') - .map((li) => `
  • ${li.replace(/^[*\-] /, '')}
  • `) - .join('') - return `
      ${items}
    ` - }) - // Line breaks - .replace(/\n\n/g, '

    ') - .replace(/\n/g, '
    ') - // Wrap in paragraph - .replace(/^(.*)$/, '

    $1

    ') - : 'Check out the latest updates!', - }} - /> - {releaseNotes?.html_url && ( - - - View full release on GitHub - - - )} -
    - - - -
    ) } diff --git a/app/client/src/views/ImageFeed.js b/app/client/src/views/ImageFeed.js index fbcb8a75..51af3ff1 100644 --- a/app/client/src/views/ImageFeed.js +++ b/app/client/src/views/ImageFeed.js @@ -17,6 +17,7 @@ import { import EditIcon from '@mui/icons-material/Edit' import CheckIcon from '@mui/icons-material/Check' import DeleteIcon from '@mui/icons-material/Delete' +import CasinoIcon from '@mui/icons-material/Casino' import Select from 'react-select' import ImageCards from '../components/cards/ImageCards' import EditImageModal from '../components/modal/EditImageModal' @@ -34,6 +35,8 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o const [alert, setAlert] = React.useState({ open: false }) const [modalImage, setModalImage] = React.useState(null) const [sortOrder, setSortOrder] = React.useState(SORT_OPTIONS?.[0] || { value: 'newest', label: 'Newest' }) + const [randomized, setRandomized] = React.useState(false) + const [randomizedImages, setRandomizedImages] = React.useState([]) const [toolbarTarget, setToolbarTarget] = React.useState(null) // Edit mode state @@ -135,6 +138,19 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o }) }, [displayImages, sortOrder]) + const finalImages = randomized ? randomizedImages : sortedImages + + const handleSortChange = (option) => { + setSortOrder(option) + setRandomized(false) + } + + const handleRandomize = () => { + const shuffled = [...displayImages].sort(() => Math.random() - 0.5) + setRandomizedImages(shuffled) + setRandomized(true) + } + const handleImageOpen = React.useCallback((image) => { setModalImage(image) }, []) @@ -154,11 +170,11 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o }) }, []) - const allSelected = sortedImages.length > 0 && selectedImages.size === sortedImages.length + const allSelected = finalImages.length > 0 && selectedImages.size === finalImages.length const handleSelectAllToggle = () => { if (allSelected) setSelectedImages(new Set()) - else setSelectedImages(new Set(sortedImages.map((img) => img.image_id))) + else setSelectedImages(new Set(finalImages.map((img) => img.image_id))) } const handleDeleteClick = () => setDeleteDialogOpen(true) @@ -188,18 +204,18 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o const handleNext = React.useCallback(() => { setModalImage((cur) => { if (!cur) return cur - const idx = sortedImages.findIndex((img) => img.image_id === cur.image_id) - return idx >= 0 && idx < sortedImages.length - 1 ? sortedImages[idx + 1] : cur + const idx = finalImages.findIndex((img) => img.image_id === cur.image_id) + return idx >= 0 && idx < finalImages.length - 1 ? finalImages[idx + 1] : cur }) - }, [sortedImages]) + }, [finalImages]) const handlePrev = React.useCallback(() => { setModalImage((cur) => { if (!cur) return cur - const idx = sortedImages.findIndex((img) => img.image_id === cur.image_id) - return idx > 0 ? sortedImages[idx - 1] : cur + const idx = finalImages.findIndex((img) => img.image_id === cur.image_id) + return idx > 0 ? finalImages[idx - 1] : cur }) - }, [sortedImages]) + }, [finalImages]) const handleModalClose = (update) => { if (update) { @@ -231,17 +247,36 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o ReactDOM.createPortal( {!(editMode && isMdDown) && ( - - + + + + )} {authenticated && ( @@ -311,7 +346,7 @@ const ImageFeed = ({ authenticated, searchText, cardSize, selectedImageFolder, o {loading && } {!loading && ( ') @@ -108,7 +116,7 @@ def config(): ) public_config["transcoding_enabled"] = current_app.config.get('ENABLE_TRANSCODING', False) limit_mb = current_app.config.get('DEMO_UPLOAD_LIMIT_MB', 0) - if limit_mb > 0: + if limit_mb > 0 and current_app.config.get('DEMO_MODE'): public_config["upload_limit_mb"] = limit_mb return public_config else: @@ -137,6 +145,28 @@ def get_release_notes(): }) +@api.route('/api/releases') +def get_releases(): + """Return a paginated list of GitHub releases from cache.""" + try: + limit = min(int(request.args.get('limit', 3)), 10) + offset = max(int(request.args.get('offset', 0)), 0) + except (ValueError, TypeError): + return jsonify({'error': 'Invalid parameters'}), 400 + + releases = _fetch_releases() + total = len(releases) + page = releases[offset:offset + limit] + + return jsonify({ + 'releases': page, + 'total': total, + 'offset': offset, + 'limit': limit, + 'has_more': offset + limit < total, + }) + + @api.route('/api/user/last-seen-version', methods=["PUT"]) @login_required def user_last_seen_version(): diff --git a/app/server/fireshare/api/scan.py b/app/server/fireshare/api/scan.py index 423ff09a..8a337a89 100644 --- a/app/server/fireshare/api/scan.py +++ b/app/server/fireshare/api/scan.py @@ -65,7 +65,8 @@ def folder_size(): paths = current_app.config['PATHS'] video_path = str(paths['video']) derived_path = str(paths['processed'] / 'derived') - size_bytes = get_folder_size(video_path, derived_path) + image_path = str(paths['image']) + size_bytes = get_folder_size(video_path, derived_path, image_path) size_mb = size_bytes / (1024 * 1024) if size_mb < 1024: @@ -78,7 +79,7 @@ def folder_size(): size_tb = size_mb / (1024 * 1024) size_pretty = f"{round(size_tb, 1)} TB" - result = {"folders": [video_path, derived_path], "size_bytes": size_bytes, "size_pretty": size_pretty} + result = {"folders": [video_path, derived_path, image_path], "size_bytes": size_bytes, "size_pretty": size_pretty} _folder_size_cache['result'] = result _folder_size_cache['expires_at'] = time.time() + _FOLDER_SIZE_TTL return jsonify(result) diff --git a/app/server/fireshare/api/upload.py b/app/server/fireshare/api/upload.py index 2809c980..8e24f77f 100644 --- a/app/server/fireshare/api/upload.py +++ b/app/server/fireshare/api/upload.py @@ -19,6 +19,8 @@ def _check_upload_size(file_size_bytes): """Return a 413 Response if the file exceeds DEMO_UPLOAD_LIMIT_MB, else None.""" + if not current_app.config.get('DEMO_MODE'): + return None limit_mb = current_app.config.get('DEMO_UPLOAD_LIMIT_MB', 0) if limit_mb and limit_mb > 0: if file_size_bytes > limit_mb * 1024 * 1024: diff --git a/app/server/fireshare/auth.py b/app/server/fireshare/auth.py index f842858d..3917d8ac 100644 --- a/app/server/fireshare/auth.py +++ b/app/server/fireshare/auth.py @@ -126,37 +126,32 @@ def loggedin(): if not current_user.is_authenticated: return jsonify({'authenticated': False}) - # Check if a newer release exists and user hasn't dismissed it yet - show_release_notes = False - release_notes = None release_data = _fetch_release_notes() local_version = _get_local_version() + latest_release = None if release_data and local_version: latest_version = release_data['version'] update_available = tuple(int(x) for x in latest_version.split('.')) > tuple(int(x) for x in local_version.split('.')) if update_available: current_app.logger.info(f"A new version of Fireshare is available! You have v{local_version}, latest is v{latest_version}.") + is_dev = current_app.config.get('ENVIRONMENT') == 'dev' + release_is_old_enough = is_dev + if not is_dev: + try: + published_dt = datetime.fromisoformat(release_data.get('published_at', '').replace('Z', '+00:00')) + release_is_old_enough = (datetime.now(timezone.utc) - published_dt).total_seconds() >= 86400 + except (ValueError, TypeError): + pass + if release_is_old_enough: + latest_release = release_data else: current_app.logger.info(f"Fireshare is up to date (v{local_version}).") - published_at = release_data.get('published_at', '') - release_is_old_enough = False - if published_at: - try: - published_dt = datetime.fromisoformat(published_at.replace('Z', '+00:00')) - release_age = datetime.now(timezone.utc) - published_dt - release_is_old_enough = release_age.total_seconds() >= 86400 - except (ValueError, TypeError): - release_is_old_enough = True - if update_available and release_is_old_enough and current_user.last_seen_version != latest_version: - show_release_notes = True - release_notes = release_data return jsonify({ 'authenticated': True, 'admin': current_user.admin, - 'show_release_notes': show_release_notes, - 'release_notes': release_notes + 'latest_release': latest_release, }) @auth.route('/api/logout', methods=['POST']) diff --git a/app/server/fireshare/cli.py b/app/server/fireshare/cli.py index 483319df..7dbcdd12 100755 --- a/app/server/fireshare/cli.py +++ b/app/server/fireshare/cli.py @@ -247,18 +247,18 @@ def scan_videos(root): existing = next((vr for vr in video_rows if vr.video_id == video_id), None) duplicate = next((dvr for dvr in new_videos if dvr.video_id == video_id), None) if duplicate: - logger.info(f"Found duplicate video {video_id} as {str(path)}, skipping...") + logger.debug(f"Found duplicate video {video_id} as {str(path)}, skipping...") elif existing: if not existing.available: - logger.info(f"Updating Video {video_id}, available=True") + logger.debug(f"Updating Video {video_id}, available=True") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "available": True }) if not existing.created_at: created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}")) - logger.info(f"Updating Video {video_id}, created_at={created_at}") + logger.debug(f"Updating Video {video_id}, created_at={created_at}") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "created_at": created_at }) if not existing.updated_at: updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}")) - logger.info(f"Updating Video {video_id}, updated_at={updated_at}") + logger.debug(f"Updating Video {video_id}, updated_at={updated_at}") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "updated_at": updated_at }) else: created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}")) @@ -283,7 +283,7 @@ def scan_videos(root): prefix = "../" * num_up rel_src = Path(prefix + str(src).replace(str(common_root), '')) if not dst.exists(): - logger.info(f"Linking {str(rel_src)} --> {str(dst)}") + logger.debug(f"Linking {str(rel_src)} --> {str(dst)}") try: os.symlink(src, dst, dir_fd=fd) except FileExistsError: @@ -339,25 +339,25 @@ def scan_videos(root): pending_suggestions = {} for nv in videos_needing_detection: filename = Path(nv.path).stem - logger.info(f"[Game Detection] Video: {nv.video_id}, Path: {nv.path}, Filename: {filename}") + logger.debug(f"[Game Detection] Video: {nv.video_id}, Path: {nv.path}, Filename: {filename}") detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key, path=nv.path) if detected_game: - logger.info(f"[Game Detection] Result: {detected_game['game_name']} (confidence: {detected_game['confidence']:.2f}, source: {detected_game['source']})") + logger.debug(f"[Game Detection] Result: {detected_game['game_name']} (confidence: {detected_game['confidence']:.2f}, source: {detected_game['source']})") if detected_game['confidence'] >= 0.65: pending_suggestions[nv.video_id] = detected_game - logger.info(f"[Game Detection] Queued suggestion for {nv.video_id}") + logger.debug(f"[Game Detection] Queued suggestion for {nv.video_id}") else: - logger.info(f"[Game Detection] Confidence too low, skipping suggestion") + logger.debug(f"[Game Detection] Confidence too low, skipping suggestion") else: - logger.info(f"[Game Detection] No match found for {nv.video_id}") + logger.debug(f"[Game Detection] No match found for {nv.video_id}") # Batch save all suggestions at once if pending_suggestions: save_game_suggestions_batch(pending_suggestions) logger.info(f"[Game Detection] Saved {len(pending_suggestions)} suggestion(s) in batch") existing_videos = Video.query.filter_by(available=True).all() - logger.info(f"Verifying {len(existing_videos):,} video files still exist...") + logger.debug(f"Verifying {len(existing_videos):,} video files still exist...") for ev in existing_videos: file_path = Path((paths["video"] / ev.path).absolute()) logger.debug(f"Verifying video {ev.video_id} at {file_path} is available") @@ -418,15 +418,15 @@ def scan_video(ctx, path, tag_ids, game_id, title): existing = next((vr for vr in video_rows if vr.video_id == video_id), None) if existing: if not existing.available: - logger.info(f"Updating Video {video_id}, available=True") + logger.debug(f"Updating Video {video_id}, available=True") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "available": True }) if not existing.created_at: created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}")) - logger.info(f"Updating Video {video_id}, created_at={created_at}") + logger.debug(f"Updating Video {video_id}, created_at={created_at}") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "created_at": created_at }) if not existing.updated_at: updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}")) - logger.info(f"Updating Video {video_id}, updated_at={updated_at}") + logger.debug(f"Updating Video {video_id}, updated_at={updated_at}") db.session.query(Video).filter_by(video_id=existing.video_id).update({ "updated_at": updated_at }) else: created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}")) @@ -443,11 +443,11 @@ def scan_video(ctx, path, tag_ids, game_id, title): prefix = "../" * num_up rel_src = Path(prefix + str(src).replace(str(common_root), '')) if not dst.exists(): - logger.info(f"Linking {str(rel_src)} --> {str(dst)}") + logger.debug(f"Linking {str(rel_src)} --> {str(dst)}") try: os.symlink(src, dst, dir_fd=fd) except FileExistsError: - logger.info(f"{dst} exists already") + logger.debug(f"{dst} exists already") info = VideoInfo(video_id=v.video_id, title=title or Path(v.path).stem, private=video_config["private"]) db.session.add(info) db.session.commit() @@ -477,7 +477,7 @@ def scan_video(ctx, path, tag_ids, game_id, title): # Automatic game detection (skip if already auto-tagged) if not auto_tagged: - logger.info("Attempting automatic game detection...") + logger.debug("Attempting automatic game detection...") steamgriddb_api_key = config.get("integrations", {}).get("steamgriddb_api_key") filename = Path(v.path).stem detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key, path=v.path) @@ -486,14 +486,14 @@ def scan_video(ctx, path, tag_ids, game_id, title): save_game_suggestion(v.video_id, detected_game) logger.info(f"Created game suggestion for video {v.video_id}: {detected_game['game_name']} (confidence: {detected_game['confidence']:.2f}, source: {detected_game['source']})") else: - logger.info(f"No confident game match found for video {v.video_id}") + logger.debug(f"No confident game match found for video {v.video_id}") - logger.info("Syncing metadata") + logger.debug("Syncing metadata") ctx.invoke(sync_metadata, video=video_id) info = VideoInfo.query.filter(VideoInfo.video_id==video_id).one() processed_root = Path(current_app.config['PROCESSED_DIRECTORY']) - logger.info(f"Checking for videos with missing posters...") + logger.debug(f"Checking for videos with missing posters...") derived_path = Path(processed_root, "derived", info.video_id) video_path = Path(processed_root, "video_links", info.video_id + video_file.suffix) if video_path.exists(): @@ -561,7 +561,7 @@ def repair_symlinks(): prefix = "../" * num_up rel_src = Path(prefix + str(src).replace(str(common_root), '')) if not dst.exists(): - logger.info(f"Linking {str(rel_src)} --> {str(dst)}") + logger.debug(f"Linking {str(rel_src)} --> {str(dst)}") try: os.symlink(src, dst, dir_fd=fd) except FileExistsError: @@ -609,7 +609,7 @@ def sync_metadata(video): else: duration = 0 width, height = int(vcodec['width']), int(vcodec['height']) - logger.info(f'Scanned {v.video_id} duration={duration}s, resolution={width}x{height}: {v.video.path}') + logger.debug(f'Scanned {v.video_id} duration={duration}s, resolution={width}x{height}: {v.video.path}') v.info = json.dumps(info) v.duration = duration v.width = width @@ -642,11 +642,11 @@ def create_web_videos(): prefix = "../" * num_up rel_src = Path(prefix + str(out_mp4_fn).replace(str(common_root), '')) if not dst.exists(): - logger.info(f"Linking {str(rel_src)} --> {str(dst)}") + logger.debug(f"Linking {str(rel_src)} --> {str(dst)}") try: os.symlink(out_mp4_fn, dst, dir_fd=fd) except FileExistsError: - logger.info(f"{dst} exists already") + logger.debug(f"{dst} exists already") else: logger.debug(f"Skipping {v.video_id} because {str(out_mp4_fn)} already exists") @@ -661,7 +661,7 @@ def create_posters(regenerate, skip): with create_app().app_context(): processed_root = Path(current_app.config['PROCESSED_DIRECTORY']) vinfos = VideoInfo.query.all() - logger.info(f"Checking for videos with missing posters...") + logger.debug(f"Checking for videos with missing posters...") for vi in vinfos: derived_path = Path(processed_root, "derived", vi.video_id) video_path = Path(processed_root, "video_links", vi.video_id + vi.video.extension) @@ -702,7 +702,7 @@ def create_boomerang_posters(regenerate): derived_path.mkdir(parents=True) util.create_boomerang_preview(video_path, poster_path) else: - logger.info(f"Skipping creation of boomerang poster for video {vi.video_id} because it exists at {str(poster_path)}") + logger.debug(f"Skipping creation of boomerang poster for video {vi.video_id} because it already exists") @cli.command() @click.option("--regenerate", "-r", help="Overwrite existing transcoded videos", is_flag=True) @@ -785,7 +785,7 @@ def handle_cancel(signum, frame): if original_height > 0 and original_height <= height: skipped_source_too_small += 1 if video: - logger.info( + logger.debug( f"Skipping {vi.video_id} {height}p: source height ({original_height}p) " f"is not greater than target" ) @@ -799,12 +799,12 @@ def handle_cancel(signum, frame): reconciled_flag_updates += 1 reconciled_videos.add(vi.video_id) if video: - logger.info(f"Detected existing {height}p output on disk; updating {has_attr}=True for {vi.video_id}") + logger.debug(f"Detected existing {height}p output on disk; updating {has_attr}=True for {vi.video_id}") if output_exists and not regenerate: skipped_existing_output += 1 if video: - logger.info(f"Skipping {vi.video_id} {height}p: output already exists at {transcode_path}") + logger.debug(f"Skipping {vi.video_id} {height}p: output already exists at {transcode_path}") continue work_items.append((vi, height, video_path, derived_path, transcode_path)) @@ -985,7 +985,7 @@ def scan_images(root): existing = next((ir for ir in image_rows if ir.image_id == iid), None) duplicate = next((ni for ni in new_images if ni.image_id == iid), None) if duplicate: - logger.info(f"Found duplicate image {iid} at {rel_path}, skipping...") + logger.debug(f"Found duplicate image {iid} at {rel_path}, skipping...") elif existing: if not existing.available: db.session.query(Image).filter_by(image_id=iid).update({"available": True}) @@ -1013,7 +1013,7 @@ def scan_images(root): if not info.file_size: info.file_size = src.stat().st_size db.session.commit() - logger.info(f"Regenerated derived data for existing image {iid}") + logger.debug(f"Regenerated derived data for existing image {iid}") else: created_at = datetime.fromtimestamp(os.path.getctime(str(img_file))) updated_at = datetime.fromtimestamp(os.path.getmtime(str(img_file))) @@ -1033,11 +1033,11 @@ def scan_images(root): src = Path((images_path / ni.path).absolute()) dst = image_links / (ni.image_id + ni.extension) if not dst.exists(): - logger.info(f"Linking {src} --> {dst}") + logger.debug(f"Linking {src} --> {dst}") try: os.symlink(src, dst, dir_fd=fd) except FileExistsError: - logger.info(f"{dst} exists already") + logger.debug(f"{dst} exists already") info = ImageInfo(image_id=ni.image_id, title=Path(ni.path).stem, private=image_config.get("private", True)) db.session.add(info) @@ -1058,7 +1058,7 @@ def scan_images(root): link = ImageGameLink(image_id=ni.image_id, game_id=game_id, created_at=datetime.utcnow()) db.session.add(link) auto_tagged += 1 - logger.info(f"[Image Folder Rule] Auto-tagged {ni.image_id} to game {game_id} (folder: {folder})") + logger.debug(f"[Image Folder Rule] Auto-tagged {ni.image_id} to game {game_id} (folder: {folder})") if auto_tagged: db.session.commit() logger.info(f"Auto-tagged {auto_tagged} image(s) via image folder rules") @@ -1162,7 +1162,7 @@ def scan_image(ctx, path, game_id, tag_ids, title): db.session.commit() logger.info(f"Regenerated derived data for existing image {iid}") else: - logger.info(f"Image {iid} already indexed") + logger.debug(f"Image {iid} already indexed") else: created_at = datetime.fromtimestamp(os.path.getctime(str(img_file))) updated_at = datetime.fromtimestamp(os.path.getmtime(str(img_file))) diff --git a/app/server/fireshare/util.py b/app/server/fireshare/util.py index 61afd07c..3e1f4cc6 100755 --- a/app/server/fireshare/util.py +++ b/app/server/fireshare/util.py @@ -305,10 +305,10 @@ def _run_decode_test(decoder=None): # If libaom can't handle this bitstream, try dav1d as a fallback if libaom_unsupported: if check_dav1d_available(): - logger.info("libaom cannot decode this AV1 bitstream, retrying with dav1d...") + logger.debug("libaom cannot decode this AV1 bitstream, retrying with dav1d...") dav1d_result = _run_decode_test(decoder='libdav1d') if dav1d_result.returncode == 0: - logger.info("AV1 file validated successfully with dav1d decoder") + logger.debug("AV1 file validated successfully with dav1d decoder") return True, None, 'libdav1d' dav1d_stderr = dav1d_result.stderr.strip() if dav1d_result.stderr else "" return False, f"AV1 decode failed with both libaom and dav1d: {dav1d_stderr[:200]}", None @@ -434,7 +434,7 @@ def create_poster(video_path, out_path, second=0): logger.debug(f"$ {' '.join(cmd)}") sp.call(cmd) e = time.time() - logger.info(f'Generated poster {str(out_path)} in {e-s}s') + logger.debug(f'Generated poster {str(out_path)} in {e-s}s') # --------------------------------------------------------------------------- @@ -467,7 +467,7 @@ def create_image_webp(src_path: Path, out_path: Path, quality: int = 90) -> bool img = img.convert('RGBA') if img.mode in ('RGBA', 'LA', 'P') else img.convert('RGB') img.save(str(out_path), 'WEBP', quality=quality, method=4) e = time.time() - logger.info(f'Created full-quality WebP {str(out_path)} in {e - s:.2f}s') + logger.debug(f'Created full-quality WebP {str(out_path)} in {e - s:.2f}s') return True except Exception as ex: logger.error(f'Failed to create WebP {str(out_path)}: {ex}') @@ -489,7 +489,7 @@ def create_image_thumbnail(src_path: Path, out_path: Path, max_width: int = 400, img = img.resize((max_width, new_height)) img.save(str(out_path), 'WEBP', quality=quality, method=4) e = time.time() - logger.info(f'Created thumbnail {str(out_path)} in {e - s:.2f}s') + logger.debug(f'Created thumbnail {str(out_path)} in {e - s:.2f}s') return True except Exception as ex: logger.error(f'Failed to create thumbnail {str(out_path)}: {ex}') @@ -647,12 +647,12 @@ def check_nvenc_available(encoder=None): def transcode_video(video_path, out_path): s = time.time() - logger.info(f"Transcoding video") + logger.debug(f"Transcoding video") cmd = ['ffmpeg', '-v', 'quiet', '-y', '-i', str(video_path), '-c:v', 'libx264', '-c:a', 'aac', str(out_path)] logger.debug(f"$: {' '.join(cmd)}") sp.call(cmd) e = time.time() - logger.info(f'Transcoded {str(out_path)} in {e-s}s') + logger.debug(f'Transcoded {str(out_path)} in {e-s}s') def _get_encoder_candidates(use_gpu=False, encoder_preference='auto'): """ @@ -871,7 +871,7 @@ def transcode_video_quality(video_path, out_path, height, use_gpu=False, timeout logger.warning("Skipping transcoding for this video due to file corruption or read errors") return (False, 'corruption') if preferred_decoder: - logger.info(f"Using {preferred_decoder} as input decoder for this source file") + logger.debug(f"Using {preferred_decoder} as input decoder for this source file") # Get video duration for progress logging total_duration = get_video_duration(video_path) or 0 @@ -880,7 +880,7 @@ def transcode_video_quality(video_path, out_path, height, use_gpu=False, timeout if timeout_seconds is None: timeout_seconds = calculate_transcode_timeout(video_path) - logger.info(f"Using transcode timeout of {timeout_seconds}s ({timeout_seconds/60:.1f} minutes)") + logger.debug(f"Using transcode timeout of {timeout_seconds}s ({timeout_seconds/60:.1f} minutes)") # Determine output container based on codec out_path_str = str(out_path) @@ -1041,7 +1041,7 @@ def transcode_video_quality(video_path, out_path, height, use_gpu=False, timeout last_exception = None for encoder in encoders: - logger.info(f"Trying {encoder['name']}...") + logger.debug(f"Trying {encoder['name']}...") # Build ffmpeg command targeting the temp path cmd = _build_transcode_command(video_path, tmp_path, height, encoder, input_decoder=preferred_decoder) @@ -1107,11 +1107,11 @@ def create_boomerang_preview(video_path, out_path, clip_duration=5): '-i', str(video_path), '-y', '-vf', 'scale=-2:480', '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '28', '-an', '-movflags', '+faststart', str(out_path)] - logger.info(f"Creating boomerang preview") + logger.debug(f"Creating boomerang preview") logger.debug(f"$: {' '.join(cmd)}") sp.call(cmd) e = time.time() - logger.info(f'Generated boomerang preview {str(out_path)} in {e-s}s') + logger.debug(f'Generated boomerang preview {str(out_path)} in {e-s}s') def dur_string_to_seconds(dur: str) -> float: if type(dur) == int: return float(dur) @@ -1198,7 +1198,7 @@ def detect_game_from_filename(filename: str, steamgriddb_api_key: str = None, pa # Try SteamGridDB with folder name if steamgriddb_api_key: - logger.info(f"No local folder match, searching SteamGridDB for folder: '{folder_name}'") + logger.debug(f"No local folder match, searching SteamGridDB for folder: '{folder_name}'") from fireshare.steamgrid import SteamGridDBClient client = SteamGridDBClient(steamgriddb_api_key) @@ -1267,7 +1267,7 @@ def detect_game_from_filename(filename: str, steamgriddb_api_key: str = None, pa # Step 2: Fallback to SteamGridDB search if steamgriddb_api_key: - logger.info(f"No local match found, searching SteamGridDB for: '{clean_name}'") + logger.debug(f"No local match found, searching SteamGridDB for: '{clean_name}'") from fireshare.steamgrid import SteamGridDBClient client = SteamGridDBClient(steamgriddb_api_key) diff --git a/app/server/gunicorn.conf.py b/app/server/gunicorn.conf.py index 5de9d872..d72d210a 100644 --- a/app/server/gunicorn.conf.py +++ b/app/server/gunicorn.conf.py @@ -41,14 +41,70 @@ # Worker tmp directory worker_tmp_dir = "/dev/shm" # Use RAM for worker tmp files +# Sentinel file used to elect exactly one worker as the scheduler worker. +# Uses /dev/shm (already our worker_tmp_dir) which is guaranteed writable. +# Written with O_EXCL so the first worker to create it wins atomically. +_SCHEDULER_SENTINEL = "/dev/shm/fireshare_scheduler.lock" + + def on_starting(server): """Called just before the master process is initialized.""" + # Remove a stale sentinel from a previous run so the first worker of this + # run can cleanly (re-)claim the scheduler role. + try: + os.unlink(_SCHEDULER_SENTINEL) + except Exception: + pass server.log.info("Starting Fireshare") + +def post_fork(server, worker): + """Elect exactly one worker to run the background scheduler. + + Each worker races to create the sentinel file with O_CREAT|O_EXCL, which + is atomic on POSIX filesystems. The winner sets FIRESHARE_START_SCHEDULER + in its own environment; losers leave it unset. create_app() reads this + env var to decide whether to call init_schedule(). + + Any failure here is logged and swallowed so hook errors never crash workers. + """ + try: + fd = os.open(_SCHEDULER_SENTINEL, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + os.write(fd, str(os.getpid()).encode()) + os.close(fd) + os.environ['FIRESHARE_START_SCHEDULER'] = '1' + server.log.info(f"Worker {os.getpid()}: elected as scheduler worker") + except FileExistsError: + # Another worker already claimed the scheduler role + os.environ.pop('FIRESHARE_START_SCHEDULER', None) + except Exception as e: + # Sentinel creation failed (permissions, missing dir, etc.) — degrade + # gracefully: no worker will run the scheduler rather than crashing. + server.log.warning(f"Worker {os.getpid()}: could not create scheduler sentinel: {e}") + os.environ.pop('FIRESHARE_START_SCHEDULER', None) + + +def worker_exit(server, worker): + """When the scheduler worker exits, clear the sentinel so a replacement + worker can take over the scheduler role on its next startup.""" + try: + with open(_SCHEDULER_SENTINEL, 'r') as f: + scheduler_pid = int(f.read().strip()) + if scheduler_pid == worker.pid: + os.unlink(_SCHEDULER_SENTINEL) + server.log.info( + f"Scheduler worker {worker.pid} exited; sentinel cleared " + "so a new worker can take over" + ) + except Exception: + pass + + def when_ready(server): """Called just after the server is started.""" server.log.info("Fireshare is ready") + def on_reload(server): """Called to recycle workers during a reload.""" server.log.info("Reloading Fireshare")