210 lines
6.0 KiB
JavaScript
210 lines
6.0 KiB
JavaScript
(() => {
|
|
const DEFAULT_BASE_URL = (() => {
|
|
try {
|
|
return new URL(document.currentScript?.src || window.location.href).origin;
|
|
} catch {
|
|
return window.location.origin;
|
|
}
|
|
})();
|
|
|
|
const STYLE = `
|
|
:host {
|
|
display: block;
|
|
width: var(--rbw-width, 100%);
|
|
height: var(--rbw-height, 260px);
|
|
}
|
|
.frame {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border-radius: var(--rbw-radius, 14px);
|
|
background: #eef2f7;
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
|
}
|
|
img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: var(--rbw-fit, cover);
|
|
filter: blur(var(--rbw-blur, 0px));
|
|
transform: scale(1.04);
|
|
opacity: 0;
|
|
transition: opacity 180ms ease;
|
|
will-change: filter, opacity, transform;
|
|
}
|
|
.state {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 16px;
|
|
text-align: center;
|
|
color: #5b657a;
|
|
font: 13px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
background: linear-gradient(180deg, rgba(255,255,255,.45), rgba(255,255,255,.2));
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
:host([loaded]) .state,
|
|
:host([error]) .state {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
:host([loaded]) img {
|
|
opacity: 1;
|
|
}
|
|
`;
|
|
|
|
const DEFAULTS = {
|
|
mode: 'auto',
|
|
blur: '0',
|
|
radius: '14px',
|
|
fit: 'cover',
|
|
height: '260px',
|
|
width: '100%',
|
|
};
|
|
|
|
class RandomBackgroundWidget extends HTMLElement {
|
|
static observedAttributes = ['src', 'mode', 'blur', 'radius', 'fit', 'height', 'width'];
|
|
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: 'open' });
|
|
this._manifest = null;
|
|
this._requestSeq = 0;
|
|
this._boundRefresh = this.refresh.bind(this);
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
this.refresh();
|
|
window.addEventListener('resize', this._boundRefresh, { passive: true });
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
window.removeEventListener('resize', this._boundRefresh);
|
|
}
|
|
|
|
attributeChangedCallback() {
|
|
if (this.isConnected) {
|
|
this.render();
|
|
this.refresh();
|
|
}
|
|
}
|
|
|
|
get baseUrl() {
|
|
return (this.getAttribute('src') || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
}
|
|
|
|
get mode() {
|
|
return (this.getAttribute('mode') || DEFAULTS.mode).toLowerCase();
|
|
}
|
|
|
|
get blur() {
|
|
const raw = Number.parseFloat(this.getAttribute('blur') || DEFAULTS.blur);
|
|
return Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
}
|
|
|
|
get radius() {
|
|
return this.getAttribute('radius') || DEFAULTS.radius;
|
|
}
|
|
|
|
get fit() {
|
|
return this.getAttribute('fit') || DEFAULTS.fit;
|
|
}
|
|
|
|
get height() {
|
|
return this.getAttribute('height') || DEFAULTS.height;
|
|
}
|
|
|
|
get width() {
|
|
return this.getAttribute('width') || DEFAULTS.width;
|
|
}
|
|
|
|
render() {
|
|
if (!this.shadowRoot) return;
|
|
this.style.setProperty('--rbw-width', this.width);
|
|
this.style.setProperty('--rbw-height', this.height);
|
|
this.style.setProperty('--rbw-radius', this.radius);
|
|
this.style.setProperty('--rbw-fit', this.fit);
|
|
this.style.setProperty('--rbw-blur', `${this.blur}px`);
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>${STYLE}</style>
|
|
<div class="frame">
|
|
<img part="image" alt="random background">
|
|
<div class="state">加载中</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async refresh() {
|
|
const seq = ++this._requestSeq;
|
|
const img = this.shadowRoot.querySelector('img');
|
|
const state = this.shadowRoot.querySelector('.state');
|
|
|
|
try {
|
|
if (!this._manifest) {
|
|
this._manifest = await this.fetchManifest();
|
|
}
|
|
if (seq !== this._requestSeq) return;
|
|
|
|
const variant = this.resolveVariant();
|
|
const data = this._manifest?.variants?.[variant] || {};
|
|
const folder = data.folder || (variant === 'mobile' ? 'mobile-image' : 'desketop-image');
|
|
const images = Array.isArray(data.images) ? data.images.filter((item) => typeof item === 'string') : [];
|
|
|
|
if (!images.length) {
|
|
throw new Error(`no images in ${folder}`);
|
|
}
|
|
|
|
const filename = images[Math.floor(Math.random() * images.length)];
|
|
img.src = `${this.baseUrl}/${folder}/${filename}`;
|
|
img.alt = `random background ${variant}`;
|
|
this.setAttribute('loaded', '');
|
|
this.removeAttribute('error');
|
|
state.textContent = '';
|
|
} catch (error) {
|
|
this.removeAttribute('loaded');
|
|
this.setAttribute('error', '');
|
|
state.textContent = error instanceof Error ? error.message : 'load failed';
|
|
}
|
|
}
|
|
|
|
resolveVariant() {
|
|
if (this.mode === 'mobile' || this.mode === 'desktop') return this.mode;
|
|
|
|
const orientation = window.matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape';
|
|
const device = this.detectDevice();
|
|
|
|
if (orientation === 'portrait') return 'mobile';
|
|
if (orientation === 'landscape') return 'desktop';
|
|
return device;
|
|
}
|
|
|
|
detectDevice() {
|
|
if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
|
|
return navigator.userAgentData.mobile ? 'mobile' : 'desktop';
|
|
}
|
|
return /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ? 'mobile' : 'desktop';
|
|
}
|
|
|
|
async fetchManifest() {
|
|
const url = new URL('manifest.json', `${this.baseUrl}/`);
|
|
const response = await fetch(url.toString(), { mode: 'cors', cache: 'no-store' });
|
|
if (!response.ok) {
|
|
throw new Error(`manifest ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
}
|
|
|
|
if (!customElements.get('random-background-widget')) {
|
|
customElements.define('random-background-widget', RandomBackgroundWidget);
|
|
}
|
|
|
|
window.RandomBackgroundWidget = RandomBackgroundWidget;
|
|
})();
|