Files
random-background-api/widget.js
2026-03-29 10:58:33 +08:00

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;
})();