Greasy Fork is available in English.
按下 Caps Lock 時根據開/關狀態發出不同提示音,並以浮動的符號表示目前狀態
// ==UserScript==
// @name 現在是大寫嗎
// @name:en Is CapsLock On Or What
// @name:ja CapsLockの狀態は?
// @name:lt Ar CapsLock įjungtas
// @name:cs Je CapsLock zapnutý?
// @name:hi क्या CapsLock ऑन है
// @description 按下 Caps Lock 時根據開/關狀態發出不同提示音,並以浮動的符號表示目前狀態
// @description:en Plays different alert tones based on the Caps Lock state and displays a floating symbol to indicate the current status.
// @description:ja Caps Lockを押すとオン/オフの狀態に応じて異なる通知音が鳴り、浮動シンボルで現在の狀態を表示します。
// @description:lt Paspaudus „Caps Lock「, pasigirsta skirtingi garsiniai signalai priklausomai nuo būsenos, o slankiojantis simbolis rodo dabartinę padėtį.
// @description:cs Při stisknutí klávesy Caps Lock přehraje různé tóny podle stavu zapnutí/vypnutí a zobrazí plovoucí symbol aktuálního stavu.
// @description:hi Caps Lock दबाने पर ऑन/ऑफ स्थिति के आधार पर अलग-अलग अलर्ट टोन बजाता है और वर्तमान स्थिति को फ्लोटिंग सिंबल के साथ दिखाता है।
//
// @namespace https://github.com/Max46656
// @supportURL https://github.com/Max46656/EverythingInGreasyFork/issues/new?template=bug_report.yml&labels=bug,userscript&title=[現在是大寫嗎]Bug回報
// @author Max
// @license MPL2.0
// @version 1.4.1
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_info
// @icon https://cdn-icons-png.flaticon.com/512/9096/9096962.png
// ==/UserScript==
//icon made by www.flaticon.com
class CapsLockIndicator {
lastCapsState = null;
ringBuzzer = GM_getValue('ringBuzzer', true);
audioContext = null;
volume = GM_getValue('volume', 0.6);
duration = GM_getValue('duration', 120);
showIndicator = GM_getValue('showIndicator', true);
indicator = null;
position = GM_getValue('position', { x: 20, y: 20 });
isDragging = false;
dragOffset = { x: 0, y: 0 };
constructor() {
this.i18n = new I18n();
this.initializeCapsLockState();
this.createAudioContext();
this.createIndicator();
this.registerMenuCommands();
this.bindKeyboardEvents();
console.info(`${GM_info.script.name} (${this.i18n.detectLanguage()}) 已初始化 | 音量:${this.volume} | 顯示圖示:${this.showIndicator}`);
}
initializeCapsLockState() {
const detectState = (e) => {
const state = e.getModifierState?.('CapsLock');
if (typeof state === 'boolean' && state !== this.lastCapsState) {
this.lastCapsState = state;
this.updateIndicator(state);
console.log(`${GM_info.script.name} 透過互動事件取得初始 Caps Lock 狀態: ${state}`);
}
};
const events = ['mousemove', 'wheel', 'keydown'];
events.forEach(eventType => {
document.addEventListener(eventType, detectState, { once: true });
});
}
createAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
playSound(isOn) {
if (!this.ringBuzzer || !this.audioContext) return;
try {
const oscillator = this.audioContext.createOscillator();
const gain = this.audioContext.createGain();
const filter = this.audioContext.createBiquadFilter();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(isOn ? 880 : 440, this.audioContext.currentTime);
gain.gain.value = this.volume;
filter.type = 'lowpass';
filter.frequency.value = isOn ? 1200 : 800;
oscillator.connect(filter);
filter.connect(gain);
gain.connect(this.audioContext.destination);
oscillator.start();
setTimeout(() => {
gain.gain.linearRampToValueAtTime(0.001, this.audioContext.currentTime + 0.05);
setTimeout(() => oscillator.stop(), 100);
}, this.duration);
} catch (e) {
console.error(`${GM_info.script.name} 播放音效失敗:`, e);
}
}
bindKeyboardEvents() {
document.addEventListener('keydown', (e) => {
if (e.key === 'CapsLock') {
setTimeout(() => {
const currentState = e.getModifierState('CapsLock');
if (currentState !== this.lastCapsState) {
this.playSound(currentState);
this.updateIndicator(currentState);
this.lastCapsState = currentState;
}
}, 10);
}
});
}
createIndicator() {
if (this.indicator) this.indicator.remove();
this.indicator = document.createElement('div');
Object.assign(this.indicator.style, {
position: 'fixed',
display: this.showIndicator ? 'block' : 'none',
fontSize: '20px',
lineHeight: '1',
opacity: 0.7,
backgroundColor: 'rgba(0, 0, 0, 0)',
color: '#ffffff',
zIndex: '2147483647',
cursor: 'move',
userSelect: 'none',
left: `${this.position.x}px`,
top: `${this.position.y}px`,
transition: 'background-color 0.2s, opacity 0.2s',
pointerEvents: 'auto'
});
this.indicator.title = this.i18n.t('titleIndicator');
document.body.appendChild(this.indicator);
this.makeDraggable();
this.updateIndicator(this.lastCapsState);
}
updateIndicator(isOn) {
if (!this.indicator) return;
this.indicator.textContent = isOn ? '🔠' : '🔡';
}
makeDraggable() {
this.indicator.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
this.isDragging = true;
this.dragOffset.x = e.clientX - this.indicator.offsetLeft;
this.dragOffset.y = e.clientY - this.indicator.offsetTop;
this.indicator.style.transition = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
let x = Math.max(0, Math.min(e.clientX - this.dragOffset.x, window.innerWidth - 60));
let y = Math.max(0, Math.min(e.clientY - this.dragOffset.y, window.innerHeight - 50));
this.indicator.style.left = `${x}px`;
this.indicator.style.top = `${y}px`;
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
this.indicator.style.transition = 'background-color 0.2s, opacity 0.2s';
this.position = {
x: parseInt(this.indicator.style.left) || 20,
y: parseInt(this.indicator.style.top) || 20
};
GM_setValue('position', this.position);
}
});
this.indicator.addEventListener('dblclick', () => {
this.showIndicator = !this.showIndicator;
GM_setValue('showIndicator', this.showIndicator);
this.indicator.style.display = this.showIndicator ? 'block' : 'none';
});
}
setPosition(x, y) {
this.position = {
x: Math.max(0, Math.min(x, window.innerWidth - 60)),
y: Math.max(0, Math.min(y, window.innerHeight - 50))
};
GM_setValue('position', this.position);
if (this.indicator) {
this.indicator.style.left = `${this.position.x}px`;
this.indicator.style.top = `${this.position.y}px`;
}
}
showPositionSelector() {
const currentX = this.position.x;
const currentY = this.position.y;
const presets = [
{ name: '左上角', x: 20, y: 20 },
{ name: '右上角', x: window.innerWidth - 70, y: 20 },
{ name: '左下角', x: 20, y: window.innerHeight - 60 },
{ name: '右下角', x: window.innerWidth - 70, y: window.innerHeight - 60 },
{ name: '畫面中央', x: Math.floor(window.innerWidth / 2 - 30), y: Math.floor(window.innerHeight / 2 - 25) }
];
let message = `${this.i18n.t('promptCurrentPos')}: (${currentX}, ${currentY})\n\n`;
message += this.i18n.t('promptChoosePreset') + '\n';
message += this.i18n.t('promptPreset1') + '\n';
message += this.i18n.t('promptPreset2') + '\n';
message += this.i18n.t('promptPreset3') + '\n';
message += this.i18n.t('promptPreset4') + '\n';
message += this.i18n.t('promptPreset5') + '\n\n';
message += this.i18n.t('promptCustom');
const input = prompt(message, `${currentX},${currentY}`);
if (input === null) return;
const num = parseInt(input);
if (num >= 1 && num <= presets.length) {
const preset = presets[num - 1];
this.setPosition(preset.x, preset.y);
return;
}
const match = input.match(/^(\d+),(\d+)$/);
if (match) {
const x = parseInt(match[1]);
const y = parseInt(match[2]);
if (!isNaN(x) && !isNaN(y)) {
this.setPosition(x, y);
return;
}
}
alert(this.i18n.t('alertInvalidInput'));
}
registerMenuCommands() {
GM_registerMenuCommand(`${this.ringBuzzer ? '✓' : ' '} ${this.i18n.t('menuRingBuzzer')}`, () => {
this.ringBuzzer = !this.ringBuzzer;
GM_setValue('ringBuzzer', this.ringBuzzer);
location.reload();
});
GM_registerMenuCommand(`${this.showIndicator ? '✓' : ' '} ${this.i18n.t('menuShowIndicator')}`, () => {
this.showIndicator = !this.showIndicator;
GM_setValue('showIndicator', this.showIndicator);
if (this.indicator) this.indicator.style.display = this.showIndicator ? 'block' : 'none';
});
GM_registerMenuCommand(this.i18n.t('menuSetPosition'), () => {
this.showPositionSelector();
});
GM_registerMenuCommand(this.i18n.t('menuSoundSettings'), () => {});
GM_registerMenuCommand(`${this.i18n.t('menuVolume')} (${Math.round(this.volume * 100)}%)`, () => {
const val = parseInt(prompt(this.i18n.t('volumePrompt'), this.volume))/10;
if (!isNaN(val) && val >= 0.1 && val <= 1.0) {
this.volume = val;
GM_setValue('volume', val);
}
});
GM_registerMenuCommand(`${this.i18n.t('menuDuration')} (${this.duration}ms)`, () => {
const val = parseInt(prompt(this.i18n.t('durationPrompt'), this.duration));
if (!isNaN(val) && val >= 50 && val <= 300) {
this.duration = val;
GM_setValue('duration', val);
}
});
GM_registerMenuCommand(this.i18n.t('menuResetPosition'), () => this.setPosition(20, 20));
}
}
class I18n {
translations = {
'zh-TW': {
menuRingBuzzer: '啟用 Caps Lock 提示音',
menuShowIndicator: '顯示狀態指示器',
menuSetPosition: '設定指示器位置 (跳出視窗)',
menuSoundSettings: '── 音效設定 ──',
menuVolume: '調整音量',
menuDuration: '調整提示音長度',
menuResetPosition: '重設位置為左上角',
promptCurrentPos: '目前位置',
promptChoosePreset: '請選擇預設位置:',
promptPreset1: '1. 左上角',
promptPreset2: '2. 右上角',
promptPreset3: '3. 左下角',
promptPreset4: '4. 右下角',
promptPreset5: '5. 畫面中央',
promptCustom: '或輸入自訂座標 (格式: x,y) 例如: 150,300',
alertInvalidInput: '輸入格式錯誤!\n請輸入 1~5 或 自訂座標 (例如 150,300)',
titleIndicator: 'Caps Lock 狀態指示器\n拖曳移動 • 雙擊隱藏/顯示',
volumePrompt: '請輸入音量 (1 ~ 10)',
durationPrompt: '提示音持續時間 (50 ~ 300 ms)'
},
'en': {
menuRingBuzzer: 'Enable Caps Lock Sound',
menuShowIndicator: 'Show Status Indicator',
menuSetPosition: 'Set Indicator Position (Popup)',
menuSoundSettings: '── Sound Settings ──',
menuVolume: 'Adjust Volume',
menuDuration: 'Adjust Sound Duration',
menuResetPosition: 'Reset Position to Top-Left',
promptCurrentPos: 'Current position',
promptChoosePreset: 'Choose preset position:',
promptPreset1: '1. Top-Left',
promptPreset2: '2. Top-Right',
promptPreset3: '3. Bottom-Left',
promptPreset4: '4. Bottom-Right',
promptPreset5: '5. Center',
promptCustom: 'Or enter custom coordinates (x,y) e.g. 150,300',
alertInvalidInput: 'Invalid input!\nPlease enter 1~5 or custom coordinates (e.g. 150,300)',
titleIndicator: 'Caps Lock Status Indicator\nDrag to move • Double-click to hide/show',
volumePrompt: 'Enter volume (1 ~ 10)',
durationPrompt: 'Sound duration (50 ~ 300 ms)'
},
'ja': {
menuRingBuzzer: 'Caps Lock 音を有効にする',
menuShowIndicator: '狀態インジケーターを表示',
menuSetPosition: 'インジケーター位置を設定 (ポップアップ)',
menuSoundSettings: '── 音聲設定 ──',
menuVolume: '音量を調整',
menuDuration: '音の長さを調整',
menuResetPosition: '位置を左上にリセット',
promptCurrentPos: '現在の位置',
promptChoosePreset: 'プリセット位置を選択:',
promptPreset1: '1. 左上',
promptPreset2: '2. 右上',
promptPreset3: '3. 左下',
promptPreset4: '4. 右下',
promptPreset5: '5. 中央',
promptCustom: 'またはカスタム座標を入力 (x,y) 例: 150,300',
alertInvalidInput: '入力形式が正しくありません!\n1~5 または カスタム座標 (例: 150,300) を入力してください',
titleIndicator: 'Caps Lock 狀態インジケーター\nドラッグで移動 • ダブルクリックで表示/非表示',
volumePrompt: '音量を入力 (1 ~ 10)',
durationPrompt: '音の長さ (50 ~ 300 ms)'
},
'lt': {
menuRingBuzzer: 'Įjungti Caps Lock garsą',
menuShowIndicator: 'Rodyti būsenos indikatorių',
menuSetPosition: 'Nustatyti indikatoriaus poziciją (iššokantis langas)',
menuSoundSettings: '── Garso nustatymai ──',
menuVolume: 'Reguliuoti garsumą',
menuDuration: 'Reguliuoti garso trukmę',
menuResetPosition: 'Atstatyti poziciją į viršų kairėje',
promptCurrentPos: 'Dabartinė pozicija',
promptChoosePreset: 'Pasirinkite numatytąją poziciją:',
promptPreset1: '1. Viršuje kairėje',
promptPreset2: '2. Viršuje dešinėje',
promptPreset3: '3. Apačioje kairėje',
promptPreset4: '4. Apačioje dešinėje',
promptPreset5: '5. Centre',
promptCustom: 'Arba įveskite pasirinktines koordinates (x,y) pvz.: 150,300',
alertInvalidInput: 'Neteisinga įvestis!\nĮveskite 1~5 arba pasirinktines koordinates (pvz. 150,300)',
titleIndicator: 'Caps Lock būsenos indikatorius\nVilkite norėdami perkelti • Dukart spustelėkite norėdami paslėpti/rodyti',
volumePrompt: 'Įveskite garsumą (1 ~ 10)',
durationPrompt: 'Garso trukmė (50 ~ 300 ms)'
},
'cs': {
menuRingBuzzer: 'Povolit zvuk Caps Lock',
menuShowIndicator: 'Zobrazit indikátor stavu',
menuSetPosition: 'Nastavit pozici indikátoru (vyskakovací okno)',
menuSoundSettings: '── Nastavení zvuku ──',
menuVolume: 'Upravit hlasitost',
menuDuration: 'Upravit délku zvuku',
menuResetPosition: 'Obnovit pozici do levého horního rohu',
promptCurrentPos: 'Aktuální pozice',
promptChoosePreset: 'Vyberte přednastavenou pozici:',
promptPreset1: '1. Levý horní',
promptPreset2: '2. Pravý horní',
promptPreset3: '3. Levý dolní',
promptPreset4: '4. Pravý dolní',
promptPreset5: '5. Střed',
promptCustom: 'Nebo zadejte vlastní souřadnice (x,y) např. 150,300',
alertInvalidInput: 'Neplatný vstup!\nZadejte 1~5 nebo vlastní souřadnice (např. 150,300)',
titleIndicator: 'Indikátor stavu Caps Lock\nTáhněte pro přesun • Dvojklik pro skrytí/zobrazení',
volumePrompt: 'Zadejte hlasitost (1 ~ 10)',
durationPrompt: 'Délka zvuku (50 ~ 300 ms)'
},
'hi': {
menuRingBuzzer: 'Caps Lock ध्वनि सक्षम करें',
menuShowIndicator: 'स्थिति संकेतक दिखाएं',
menuSetPosition: 'संकेतक की स्थिति सेट करें (पॉपअप)',
menuSoundSettings: '── ध्वनि सेटिंग्स ──',
menuVolume: 'वॉल्यूम समायोजित करें',
menuDuration: 'ध्वनि अवधि समायोजित करें',
menuResetPosition: 'स्थिति को ऊपरी बाईं ओर रीसेट करें',
promptCurrentPos: 'वर्तमान स्थिति',
promptChoosePreset: 'प्रीसेट स्थिति चुनें:',
promptPreset1: '1. ऊपरी बाईं',
promptPreset2: '2. ऊपरी दाईं',
promptPreset3: '3. निचली बाईं',
promptPreset4: '4. निचली दाईं',
promptPreset5: '5. केंद्र',
promptCustom: 'या कस्टम निर्देशांक दर्ज करें (x,y) उदाहरण: 150,300',
alertInvalidInput: 'अमान्य इनपुट!\nकृपया 1~5 दर्ज करें या कस्टम निर्देशांक (जैसे 150,300)',
titleIndicator: 'Caps Lock स्थिति संकेतक\nखींचकर ले जाएं • दोहरी क्लिक छिपाने/दिखाने के लिए',
volumePrompt: 'वॉल्यूम दर्ज करें (1 ~ 10)',
durationPrompt: 'ध्वनि अवधि (50 ~ 300 ms)'
}
};
constructor() {
this.currentLang = GM_getValue('language', this.detectLanguage());
}
detectLanguage() {
const lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase();
if (lang.startsWith('zh')) return 'zh-TW';
if (lang.startsWith('ja')) return 'ja';
if (lang.startsWith('lt')) return 'lt';
if (lang.startsWith('cs')) return 'cs';
if (lang.startsWith('hi')) return 'hi';
return 'en';
}
t(key, vars = {}) {
const lang = this.translations[this.currentLang] ? this.currentLang : 'en';
let text = this.translations[lang][key] || this.translations['en'][key] || key;
Object.keys(vars).forEach(k => {
text = text.replace(`{${k}}`, vars[k]);
});
return text;
}
}
const johnTheFlagMarshal = new CapsLockIndicator();