Compare commits
18 commits
57c08b5646
...
f0b1d2281e
Author | SHA1 | Date | |
---|---|---|---|
f0b1d2281e | |||
16bd966a7d | |||
bf30ca0252 | |||
26954a3bf2 | |||
20202b5cd9 | |||
b03fee8c7e | |||
1aba4f405e | |||
7688caebe1 | |||
fcad7e2bc9 | |||
d18f43d769 | |||
059644f847 | |||
83eec2db60 | |||
3f47f47ced | |||
6bc0c55000 | |||
62d9f2d24a | |||
d79b6b6c33 | |||
c8c4facb60 | |||
7ec900b6b6 |
13 changed files with 350 additions and 40 deletions
|
@ -1,4 +1,4 @@
|
|||
class OutfitLayer extends HTMLElement {
|
||||
class OutfitViewer extends HTMLElement {
|
||||
#internals;
|
||||
|
||||
constructor() {
|
||||
|
@ -10,26 +10,98 @@ class OutfitLayer extends HTMLElement {
|
|||
setTimeout(() => this.#connectToChildren(), 0);
|
||||
}
|
||||
|
||||
#connectToChildren() {
|
||||
const playPauseToggle = document.querySelector(".play-pause-toggle");
|
||||
|
||||
// Read our initial playing state from the toggle, and subscribe to changes.
|
||||
this.#setIsPlaying(playPauseToggle.checked);
|
||||
playPauseToggle.addEventListener("change", () => {
|
||||
this.#setIsPlaying(playPauseToggle.checked);
|
||||
this.#setIsPlayingCookie(playPauseToggle.checked);
|
||||
});
|
||||
}
|
||||
|
||||
#setIsPlaying(isPlaying) {
|
||||
// TODO: Listen for changes to the child list, and add `playing` when new
|
||||
// nodes arrive, if playing.
|
||||
const thirtyDays = 60 * 60 * 24 * 30;
|
||||
if (isPlaying) {
|
||||
this.#internals.states.add("playing");
|
||||
for (const layer of this.querySelectorAll("outfit-layer")) {
|
||||
layer.play();
|
||||
}
|
||||
} else {
|
||||
this.#internals.states.delete("playing");
|
||||
for (const layer of this.querySelectorAll("outfit-layer")) {
|
||||
layer.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setIsPlayingCookie(isPlaying) {
|
||||
const thirtyDays = 60 * 60 * 24 * 30;
|
||||
const value = isPlaying ? "true" : "false";
|
||||
document.cookie = `DTIOutfitViewerIsPlaying=${value};max-age=${thirtyDays}`;
|
||||
}
|
||||
}
|
||||
|
||||
class OutfitLayer extends HTMLElement {
|
||||
#internals;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#internals = this.attachInternals();
|
||||
|
||||
// An <outfit-layer> starts in the loading state, and then might very
|
||||
// quickly decide it's not after `#connectToChildren`. This is to prevent a
|
||||
// flash of *non*-loading state, when a new layer loads in. (e.g. In the
|
||||
// time between our parent <turbo-frame> loading, which shows the loading
|
||||
// spinner; and us being marked `:state(loading)`, which shows the loading
|
||||
// spinner; we don't want the loading spinner to do its usual *immediate*
|
||||
// total fade-out; then have to fade back in again, on the usual delay.)
|
||||
this.#setStatus("loading");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
setTimeout(() => this.#connectToChildren(), 0);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("message", this.#onMessage);
|
||||
}
|
||||
|
||||
play() {
|
||||
this.#sendMessageToIframe({ type: "play" });
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.#sendMessageToIframe({ type: "pause" });
|
||||
}
|
||||
|
||||
#connectToChildren() {
|
||||
const image = this.querySelector("img");
|
||||
const iframe = this.querySelector("iframe");
|
||||
|
||||
if (image) {
|
||||
// Initialize status based on the image's current `complete` attribute,
|
||||
// then wait for load/error events to update it further if needed.
|
||||
this.#setStatus(image.complete ? "loaded" : "loading");
|
||||
image.addEventListener("load", () => this.#setStatus("loaded"));
|
||||
image.addEventListener("error", () => this.#setStatus("error"));
|
||||
this.#setStatus(image.complete ? "loaded" : "loading");
|
||||
} else if (iframe) {
|
||||
this.iframe = iframe;
|
||||
window.addEventListener("message", (m) => this.#onMessage(m));
|
||||
|
||||
// Initialize status to `loading`, and asynchronously request a status
|
||||
// message from the iframe if it managed to load before this triggers
|
||||
// (impressive, but I think I've seen it happen!). Then, wait for
|
||||
// messages or error events from the iframe to update status further if
|
||||
// needed.
|
||||
this.#setStatus("loading");
|
||||
this.#sendMessageToIframe({ type: "requestStatus" });
|
||||
window.addEventListener("message", (m) => this.#onMessage(m));
|
||||
this.iframe.addEventListener("error", () => this.#setStatus("error"));
|
||||
} else {
|
||||
throw new Error(
|
||||
`<outfit-layer> must contain an <img> or <iframe> tag`,
|
||||
);
|
||||
throw new Error(`<outfit-layer> must contain an <img> or <iframe> tag`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,11 +110,17 @@ class OutfitLayer extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
data.type === "status" &&
|
||||
["loaded", "error"].includes(data.status)
|
||||
) {
|
||||
this.#setStatus(data.status);
|
||||
if (data.type === "status") {
|
||||
if (data.status === "loaded") {
|
||||
this.#setStatus("loaded");
|
||||
this.#setHasAnimations(data.hasAnimations);
|
||||
} else if (data.status === "error") {
|
||||
this.#setStatus("error");
|
||||
} else {
|
||||
throw new Error(
|
||||
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
||||
|
@ -51,11 +129,31 @@ class OutfitLayer extends HTMLElement {
|
|||
}
|
||||
|
||||
#setStatus(newStatus) {
|
||||
this.#internals.states.clear();
|
||||
this.#internals.states.delete("loading");
|
||||
this.#internals.states.delete("loaded");
|
||||
this.#internals.states.delete("error");
|
||||
this.#internals.states.add(newStatus);
|
||||
}
|
||||
|
||||
#setHasAnimations(hasAnimations) {
|
||||
if (hasAnimations) {
|
||||
this.#internals.states.add("has-animations");
|
||||
} else {
|
||||
this.#internals.states.delete("has-animations");
|
||||
}
|
||||
}
|
||||
|
||||
#sendMessageToIframe(message) {
|
||||
if (this.iframe?.contentWindow == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The frame is sandboxed (origin == null), so send to Any origin.
|
||||
this.iframe.contentWindow.postMessage(message, "*");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("outfit-viewer", OutfitViewer);
|
||||
customElements.define("outfit-layer", OutfitLayer);
|
||||
|
||||
// Morph turbo-frames on this page, to reuse asset nodes when we want to—very
|
||||
|
|
|
@ -237,9 +237,7 @@ function onAnimationFrame() {
|
|||
|
||||
if (msSinceLastLog >= 5000) {
|
||||
const fps = numFramesSinceLastLog / (msSinceLastLog / 1000);
|
||||
console.debug(
|
||||
`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`,
|
||||
);
|
||||
console.debug(`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`);
|
||||
lastLogTime = document.timeline.currentTime;
|
||||
numFramesSinceLastLog = 0;
|
||||
}
|
||||
|
@ -276,6 +274,44 @@ function getInitialPlayingStatus() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scans the given MovieClip (or child createjs node), to see if
|
||||
* there are any animated areas.
|
||||
*/
|
||||
function hasAnimations(createjsNode) {
|
||||
return (
|
||||
// Some nodes have simple animation frames.
|
||||
createjsNode.totalFrames > 1 ||
|
||||
// Tweens are a form of animation that can happen separately from frames.
|
||||
// They expect timer ticks to happen, and they change the scene accordingly.
|
||||
createjsNode?.timeline?.tweens?.length >= 1 ||
|
||||
// And some nodes have _children_ that are animated.
|
||||
(createjsNode.children || []).some(hasAnimations)
|
||||
);
|
||||
}
|
||||
|
||||
function sendStatus() {
|
||||
if (loadingStatus === "loading") {
|
||||
sendMessage({ type: "status", status: "loading" });
|
||||
} else if (loadingStatus === "loaded") {
|
||||
sendMessage({
|
||||
type: "status",
|
||||
status: "loaded",
|
||||
hasAnimations: hasAnimations(movieClip),
|
||||
});
|
||||
} else if (loadingStatus === "error") {
|
||||
sendMessage({ type: "status", status: "error" });
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected loadingStatus ${JSON.stringify(loadingStatus)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
parent.postMessage(message, document.location.origin);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
updateCanvasDimensions();
|
||||
|
||||
|
@ -296,6 +332,8 @@ window.addEventListener("message", ({ data }) => {
|
|||
play();
|
||||
} else if (data.type === "pause") {
|
||||
pause();
|
||||
} else if (data.type === "requestStatus") {
|
||||
sendStatus();
|
||||
} else {
|
||||
throw new Error(`unexpected message: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
@ -303,19 +341,13 @@ window.addEventListener("message", ({ data }) => {
|
|||
|
||||
startMovie()
|
||||
.then(() => {
|
||||
parent.postMessage(
|
||||
{ type: "status", status: "loaded" },
|
||||
document.location.origin,
|
||||
);
|
||||
sendStatus();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(logPrefix, error);
|
||||
|
||||
loadingStatus = "error";
|
||||
parent.postMessage(
|
||||
{ type: "status", status: "error" },
|
||||
document.location.origin,
|
||||
);
|
||||
sendStatus();
|
||||
|
||||
// If loading the movie fails, show the fallback image instead, by moving
|
||||
// it out of the canvas content and into the body.
|
||||
|
|
64
app/assets/stylesheets/application/hanger-spinner.css
Normal file
64
app/assets/stylesheets/application/hanger-spinner.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
.hanger-spinner {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: 1.2s infinite hanger-spinner-swing;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: 1.6s infinite hanger-spinner-fade-pulse;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Adapted from animate.css "swing". We spend 75% of the time swinging,
|
||||
then 25% of the time pausing before the next loop.
|
||||
|
||||
We use this animation for folks who are okay with dizzy-ish motion.
|
||||
For reduced motion, we use a pulse-fade instead.
|
||||
*/
|
||||
@keyframes hanger-spinner-swing {
|
||||
15% {
|
||||
transform: rotate3d(0, 0, 1, 15deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: rotate3d(0, 0, 1, 5deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate3d(0, 0, 1, -5deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A homebrew fade-pulse animation. We use this for folks who don't
|
||||
like motion. It's an important accessibility thing!
|
||||
*/
|
||||
@keyframes hanger-spinner-fade-pulse {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
|
@ -38,31 +38,118 @@ body.items-show
|
|||
height: 16px
|
||||
width: 16px
|
||||
|
||||
.outfit-viewer
|
||||
position: relative
|
||||
outfit-viewer
|
||||
display: block
|
||||
position: relative
|
||||
width: 300px
|
||||
height: 300px
|
||||
border: 1px solid $module-border-color
|
||||
border-radius: 1em
|
||||
overflow: hidden
|
||||
|
||||
margin: 0 auto .75em
|
||||
|
||||
// There's no useful text in here, but double-clicking the play/pause
|
||||
// button can cause a weird selection state. Disable text selection.
|
||||
user-select: none
|
||||
-webkit-user-select: none
|
||||
|
||||
outfit-layer
|
||||
display: block
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
// We disable pointer-events most importantly for the iframes, which
|
||||
// will ignore our `cursor: wait` and show a plain cursor for the
|
||||
// inside of its own document. But also, the context menus for these
|
||||
// elements are kinda actively misleading, too!
|
||||
pointer-events: none
|
||||
|
||||
img, iframe
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
&:has(outfit-layer:state(loading))
|
||||
background: gray
|
||||
.loading-indicator
|
||||
position: absolute
|
||||
z-index: 1000
|
||||
bottom: 0px
|
||||
right: 4px
|
||||
padding: 8px
|
||||
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
||||
|
||||
&:has(outfit-layer:state(error))
|
||||
border-color: $error-border-color
|
||||
opacity: 0
|
||||
transition: opacity .5s
|
||||
|
||||
.play-pause-button
|
||||
position: absolute
|
||||
z-index: 1001
|
||||
left: 8px
|
||||
bottom: 8px
|
||||
display: none
|
||||
align-items: center
|
||||
justify-content: center
|
||||
color: white
|
||||
background: rgba(0, 0, 0, 0.64)
|
||||
width: 2.5em
|
||||
height: 2.5em
|
||||
border-radius: 100%
|
||||
border: 2px solid transparent
|
||||
transition: all .25s
|
||||
|
||||
.playing-label, .paused-label
|
||||
display: none
|
||||
width: 1em
|
||||
height: 1em
|
||||
|
||||
.play-pause-toggle
|
||||
// Visually hidden
|
||||
clip: rect(0 0 0 0)
|
||||
clip-path: inset(50%)
|
||||
height: 1px
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
white-space: nowrap
|
||||
width: 1px
|
||||
|
||||
&:checked ~ .playing-label
|
||||
display: block
|
||||
|
||||
&:not(:checked) ~ .paused-label
|
||||
display: block
|
||||
|
||||
&:hover, &:has(.play-pause-toggle:focus)
|
||||
border: 2px solid $module-border-color
|
||||
background: $module-bg-color
|
||||
color: $text-color
|
||||
|
||||
&:has(.play-pause-toggle:active)
|
||||
transform: translateY(2px)
|
||||
|
||||
&:has(outfit-layer:state(has-animations))
|
||||
.play-pause-button
|
||||
display: flex
|
||||
|
||||
.error-indicator
|
||||
font-size: 85%
|
||||
color: $error-color
|
||||
margin-top: .25em
|
||||
margin-bottom: .5em
|
||||
display: none
|
||||
|
||||
// When loading, fade in the loading spinner after a brief delay. (We only
|
||||
// apply the delay here, because fading *out* on load should be instant.)
|
||||
// We are loading when the <turbo-frame> is busy, or when at least one layer
|
||||
// is loading.
|
||||
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
||||
cursor: wait
|
||||
.loading-indicator
|
||||
opacity: 1
|
||||
transition-delay: 2s
|
||||
|
||||
#item-preview:has(outfit-layer:state(error))
|
||||
outfit-viewer
|
||||
border: 2px solid red
|
||||
.error-indicator
|
||||
display: block
|
||||
|
||||
.species-color-picker
|
||||
.error-icon
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#asset-canvas,
|
||||
#asset-image,
|
||||
#fallback {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
|
@ -15,7 +15,7 @@ class SwfAssetsController < ApplicationController
|
|||
)
|
||||
}
|
||||
|
||||
policy.script_src_elem -> {
|
||||
policy.script_src -> {
|
||||
src_list(
|
||||
helpers.javascript_url("lib/easeljs.min"),
|
||||
helpers.javascript_url("lib/tweenjs.min"),
|
||||
|
@ -24,7 +24,7 @@ class SwfAssetsController < ApplicationController
|
|||
)
|
||||
}
|
||||
|
||||
policy.style_src_elem -> {
|
||||
policy.style_src -> {
|
||||
src_list(
|
||||
helpers.stylesheet_url("swf_assets/show"),
|
||||
)
|
||||
|
|
|
@ -224,6 +224,10 @@ module ItemsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def outfit_viewer_is_playing
|
||||
cookies["DTIOutfitViewerIsPlaying"] == "true"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_on_pet_types(species, special_color=nil, &block)
|
||||
|
|
6
app/views/application/_hanger_spinner.html
Normal file
6
app/views/application/_hanger_spinner.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg class="hanger-spinner" viewBox="0 0 473 473">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M451.426,315.003c-0.517-0.344-1.855-0.641-2.41-0.889l-201.09-88.884v-28.879c38.25-4.6,57.136-29.835,57.136-62.28c0-35.926-25.283-63.026-59.345-63.026c-35.763,0-65.771,29.481-65.771,64.384c0,6.005,4.973,10.882,10.978,10.882c1.788,0,3.452-0.535,4.934-1.291c3.519-1.808,6.024-5.365,6.024-9.591c0-22.702,20.674-42.62,44.217-42.62c22.003,0,37.982,17.356,37.982,41.262c0,23.523-19.011,41.262-44.925,41.262c-6.005,0-10.356,4.877-10.356,10.882v21.267v21.353c0,0.21-0.421,0.383-0.401,0.593L35.61,320.55C7.181,330.792-2.554,354.095,0.554,371.881c3.194,18.293,18.704,30.074,38.795,30.074H422.26c23.782,0,42.438-12.307,48.683-32.942C477.11,348.683,469.078,326.766,451.426,315.003z M450.115,364.031c-3.452,11.427-13.607,18.8-27.846,18.8H39.349c-9.725,0-16.104-5.394-17.5-13.368c-1.587-9.104,4.265-22.032,21.831-28.42l199.531-94.583l196.844,87.65C449.303,340.717,453.434,353.072,450.115,364.031z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 979 B |
|
@ -1,4 +1,16 @@
|
|||
.outfit-viewer
|
||||
%outfit-viewer
|
||||
.loading-indicator= render partial: "hanger_spinner"
|
||||
|
||||
%label.play-pause-button{title: "Pause/play animations"}
|
||||
%input.play-pause-toggle{
|
||||
type: "checkbox",
|
||||
checked: outfit_viewer_is_playing,
|
||||
}
|
||||
%svg.playing-label{viewBox: "0 0 24 24", "aria-hidden": "true", "aria-label": "Pause"}
|
||||
%path{fill: "currentColor", d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z"}
|
||||
%svg.paused-label{viewBox: "0 0 24 24", "aria-hidden": "true", "aria-label": "Play"}
|
||||
%path{fill: "currentColor", d: "M8 5v14l11-7z"}
|
||||
|
||||
- outfit.visible_layers.each do |swf_asset|
|
||||
%outfit-layer{
|
||||
data: {
|
||||
|
@ -7,6 +19,6 @@
|
|||
},
|
||||
}
|
||||
- if swf_asset.canvas_movie?
|
||||
%iframe{src: swf_asset_path(swf_asset) + "?playing"}
|
||||
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
|
||||
- else
|
||||
= image_tag swf_asset.image_url, alt: ""
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
= turbo_frame_tag "item-preview" do
|
||||
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
|
||||
.error-indicator
|
||||
💥 We couldn't load all of this outfit. Try again?
|
||||
|
||||
= form_for item_path(@item), method: :get, class: "species-color-picker",
|
||||
data: {"is-valid": @preview_error.nil?} do |f|
|
||||
|
@ -39,6 +41,9 @@
|
|||
%li= link_to(contributor.name, user_contributions_path(contributor)) + format_contribution_count(count)
|
||||
%footer= t '.contributors.footer'
|
||||
|
||||
- content_for :stylesheets do
|
||||
= stylesheet_link_tag "application/hanger-spinner"
|
||||
|
||||
- content_for :javascripts do
|
||||
= javascript_include_tag "lib/idiomorph", async: true
|
||||
= javascript_include_tag "outfit-viewer", async: true
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||
= yield :stylesheets
|
||||
= stylesheet_link_tag "application"
|
||||
= render 'analytics'
|
||||
= yield :meta
|
||||
= open_graph_tags
|
||||
= csrf_meta_tag
|
||||
|
@ -24,6 +23,7 @@
|
|||
= javascript_include_tag 'application', defer: true
|
||||
= yield :javascripts
|
||||
= yield :head
|
||||
= render 'analytics'
|
||||
%body{:class => body_class}
|
||||
#container
|
||||
= yield :before_title
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
Embed for Asset ##{@swf_asset.id} | #{t "app_name"}
|
||||
%link{href: image_path("favicon.png"), rel: "icon"}
|
||||
|
||||
-# Load the stylesheet first, because displaying things correctly is the
|
||||
-# actual most essential thing.
|
||||
= stylesheet_link_tag "swf_assets/show", debug: false
|
||||
|
||||
-# NOTE: For all these assets, the Content-Security-Policy doesn't account
|
||||
-# for asset debug mode, so let's just opt out of it with `debug: false`!
|
||||
- if @swf_asset.canvas_movie?
|
||||
-# Load the stylesheet first, because displaying things correctly is the
|
||||
-# actual most essential thing.
|
||||
= stylesheet_link_tag "swf_assets/show", debug: false
|
||||
|
||||
-# This is optional, but preloading the sprites can help us from having
|
||||
-# to wait on all the other JS to load and set up before we start!
|
||||
- @swf_asset.canvas_movie_sprite_urls.each do |sprite_url|
|
||||
|
@ -33,4 +33,4 @@
|
|||
-# the browser won't bother to load it if it's not used.
|
||||
= image_tag @swf_asset.image_url, id: "fallback", alt: "", loading: "lazy"
|
||||
- else
|
||||
= image_tag @swf_asset.image_url, alt: ""
|
||||
= image_tag @swf_asset.image_url, alt: "", id: "asset-image"
|
|
@ -74,6 +74,7 @@ namespace :public_data do
|
|||
# The connection details for our database!
|
||||
config = ApplicationRecord.connection_db_config.configuration_hash
|
||||
args << "--host=#{config[:host]}" if config[:host]
|
||||
args << "--ssl=false" # SSL is the default for recent MariaDB; override!
|
||||
args << "--user=#{config[:username]}" if config[:username]
|
||||
args << "--password=#{config[:password]}" if config[:password]
|
||||
args << "--database=#{config.fetch(:database)}"
|
||||
|
|
Loading…
Reference in a new issue