wayBetter for X
This commit is contained in:
commit
df5dff619e
720 changed files with 147465 additions and 0 deletions
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* @name Alt-text Reminder
|
||||
* @description Reminds you when images in the composer have no description (alt text): a notice while composing and another when you press Post. It never blocks or delays sending.
|
||||
* @version 1.2.0
|
||||
* @author wayBetter
|
||||
* @permissions ui, network:none
|
||||
*/
|
||||
// X gives attached images a generic alt until a description is added (best-effort, may need updates when X changes).
|
||||
const GENERIC_ALT = /^(|image|media|photo|picture|bild|imagen|immagine|imagem|görsel|resim|画像|이미지|图片|圖片)$/i;
|
||||
|
||||
// i18n:begin
|
||||
// This plugin's English words. Translations live in the app's catalogs (plugin.<file name>.*,
|
||||
// docs/TRANSLATING.md); a plugin of your own can also pass { en: {...}, tr: {...} } to register.
|
||||
const MESSAGES = {
|
||||
"en": {
|
||||
"headsUp": "Heads up: {count, plural, one {# image} other {# images}} without alt text",
|
||||
"missing": "{count, plural, one {# image has no description (alt text)} other {# images have no description (alt text)}}"
|
||||
}
|
||||
};
|
||||
// i18n:end
|
||||
/** The words in the app's language (api.i18n); English before start() and outside the app. */
|
||||
let i18n = null;
|
||||
const t = (key, params = {}) => (i18n ? i18n.t(key, params) : String(MESSAGES.en[key] ?? key)
|
||||
.replace(/\{(\w+), plural, one \{([^{}]*)\} other \{([^{}]*)\}\}/g, (m, name, one, other) => (Number(params[name]) === 1 ? one : other).replace('#', String(params[name])))
|
||||
.replace(/\{(\w+)\}/g, (m, name) => (name in params ? String(params[name]) : m)));
|
||||
module.exports = class AltTextReminder {
|
||||
start() {
|
||||
const api = window.wayBetter.api;
|
||||
i18n = api.i18n || null;
|
||||
if (i18n) i18n.register(MESSAGES);
|
||||
this.api = api;
|
||||
this.scope = api.scope();
|
||||
this.scope.css('alt-text-reminder', `
|
||||
.bt-alt-note { position: fixed; left: 50%; top: 16px; transform: translateX(-50%); z-index: 2147483645; pointer-events: none;
|
||||
width: max-content; max-width: calc(100vw - 32px); box-sizing: border-box; text-align: center;
|
||||
padding: 8px 14px; border-radius: 999px; background: rgb(255, 212, 0); color: #0f1419; font: 600 13px/1.3 system-ui, sans-serif;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.25); }
|
||||
`);
|
||||
let frame = 0;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!frame) frame = requestAnimationFrame(() => { frame = 0; this.update(); });
|
||||
});
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['alt'] });
|
||||
this.scope.defer(() => { observer.disconnect(); if (frame) cancelAnimationFrame(frame); });
|
||||
// Capture phase, passive: only reads the composer and shows a toast; the click continues to X untouched.
|
||||
this.scope.on(document, 'click', event => {
|
||||
const button = event.target instanceof Element && event.target.closest('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]');
|
||||
if (!button) return;
|
||||
const missing = this.missing(button.closest('[role="dialog"]') || document);
|
||||
if (missing) this.api.toast(t('headsUp', { count: missing }), 3500);
|
||||
}, { capture: true, passive: true });
|
||||
this.scope.defer(() => this.note?.remove());
|
||||
this.update();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.scope.dispose();
|
||||
this.note = null;
|
||||
}
|
||||
|
||||
missing(root) {
|
||||
let count = 0;
|
||||
for (const img of root.querySelectorAll('[data-testid="attachments"] img')) {
|
||||
if (GENERIC_ALT.test((img.getAttribute('alt') || '').trim())) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
update() {
|
||||
const missing = this.missing(document);
|
||||
if (!missing) {
|
||||
this.note?.remove();
|
||||
return;
|
||||
}
|
||||
if (!this.note) {
|
||||
this.note = document.createElement('div');
|
||||
this.note.className = 'bt-alt-note';
|
||||
this.note.setAttribute('role', 'status');
|
||||
}
|
||||
const text = t('missing', { count: missing });
|
||||
// Only write on change: this node lives inside the observed tree.
|
||||
if (this.note.textContent !== text) this.note.textContent = text;
|
||||
if (!this.note.isConnected) document.body.appendChild(this.note);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue