diff --git a/index.html b/index.html index 0426ea0..cb0c0ca 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,7 @@ + @@ -83,6 +84,11 @@

theme

+ + diff --git a/src/css/index.css b/src/css/index.css index 94975d9..09ff497 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -12,7 +12,6 @@ --background-dark : #232c43; --background-very-dark: #182034; - --key-color-top : #ffffff; --key-color-bottom : #3a4764; --key-background : #eae3dc; @@ -274,3 +273,56 @@ input[type=range]::-ms-thumb { background-color: transparent; color: var(--guide-text) } + +.floating-btn { + position: fixed; + bottom: 20px; + right: 50px; + width: 60px; + height: 60px; + background-color: var(--key-blue-background); + color: white; + border: none; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: background-color 0.3s, transform 0.3s; +} + +.floating-btn:hover { + background-color: var(--key-blue-shadow); +} + +.floating-btn:active { + transform: scale(0.5); +} + +.floating-btn i { + font-size: 24px; +} + +.tooltip { + position: absolute; + top: -10px; + left: 50%; + transform: translate(-50%, 10px); + background-color: #555; + color: white; + padding: 5px 10px; + border-radius: 5px; + font-size: 14px; + white-space: nowrap; + transition: transform 0.8s, opacity 0.5s ease-in-out; + opacity: 0; + visibility: hidden; + border-bottom: 1px dotted black; +} + +.floating-btn:hover .tooltip { + transform: translate(-50%, -30px); + opacity: 1; + visibility: visible; +} diff --git a/src/js/calculator.js b/src/js/calculator.js index 057ecb8..00827c2 100644 --- a/src/js/calculator.js +++ b/src/js/calculator.js @@ -1,8 +1,12 @@ export class Calculator { - constructor(displayElement) { + constructor(displayElement = document.getElementById('display')) { this.display = displayElement } + setDisplayValue(value) { + this.display.value = value + } + handleAddInput(input) { this.display.value += input } @@ -39,4 +43,11 @@ export class Calculator { return keyActions } + + executeVoiceCommand(command, voiceActions) { + const filteredCommand = command.replace(/[^0-9+\-*/]/g, '') + + if (!voiceActions[command] && filteredCommand) return this.display.value = filteredCommand + if (voiceActions[command]) return voiceActions[command]() + } } diff --git a/src/js/guide.js b/src/js/guide.js index e25673f..6006952 100644 --- a/src/js/guide.js +++ b/src/js/guide.js @@ -13,7 +13,7 @@ export class Guide { doneLabel: 'Finalizar', } } - + loadSteps() { this.#config.steps = [ { @@ -43,15 +43,30 @@ export class Guide { intro: 'Altere o tema do aplicativo com um clique.', tooltipClass: 'custom-tooltip', highlightClass: 'custom-highlight' + }, + { + element: document.querySelector('#startVoice'), + title: 'Comandos de voz', + intro: ` +

Clique no botão para ativar os comandos de voz. Você pode usar os seguintes comandos:

+ +

Além de ditar a conta que deseja calcular, como "4 mais 4".

+ `, + tooltipClass: 'custom-tooltip', + highlightClass: 'custom-highlight' } ] } - loadConfig () { + loadConfig() { this.#intro.setOptions(this.#config) } - start () { + start() { this.#intro.start() } } diff --git a/src/js/main.js b/src/js/main.js index f6477b9..64e1b53 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,52 +1,32 @@ import { Calculator } from './calculator.js' import { ThemeManager } from './themeManager.js' import { Guide } from './guide.js' +import { Speak } from './speak.js' import { themes } from '../constants/index.js' document.addEventListener('DOMContentLoaded', () => { - const display = document.getElementById('display') - const calculator = new Calculator(display) + const calculator = new Calculator() + const themeManager = initializeThemeManager() - const btnTheme = document.getElementById('btnTheme') - const keySelectors = document.querySelectorAll('.key-selector') - const themeManager = new ThemeManager(themes, btnTheme, keySelectors) - - themeManager.setPreferColorSchemeTheme() - themeManager.applyStoredKeys() - - const guide = new Guide() - guide.loadSteps() - guide.loadConfig() - guide.start() - - document.querySelectorAll('.add-input').forEach(button => { - button.addEventListener('click', () => { - const value = button.dataset.operation || button.value - calculator.handleAddInput(value) - }) - }) - - document.querySelector('.reset').addEventListener('click', () => { - calculator.handleResetValue() - }) - - document.querySelector('.remove-last').addEventListener('click', () => { - calculator.handleRemoveLastInput() - }) + addEventListeners(calculator, themeManager) + new Speak(calculator) + initializeGuide() +}) - document.querySelector('.calculate').addEventListener('click', () => { - calculator.calculate() - }) +function initializeThemeManager() { + const themeManager = new ThemeManager(themes) + const btnTheme = themeManager.getBtnTheme() + const keySelectors = themeManager.getKeySelectors() - btnTheme.addEventListener('change', () => { - themeManager.changeThemeById(btnTheme.value) - }) + themeManager.setPreferColorSchemeTheme() + themeManager.applyStoredKeys() + btnTheme.addEventListener('change', () => themeManager.changeThemeById(btnTheme.value)) keySelectors.forEach(selector => { selector.addEventListener('click', () => { alert('Ao clicar em OK, pressione uma tecla para associar a este tema.') - + const hiddenInput = document.getElementById('hiddenInput') hiddenInput.focus() @@ -54,13 +34,40 @@ document.addEventListener('DOMContentLoaded', () => { themeManager.setKeyForTheme(selector, event.key) document.removeEventListener('keydown', captureKey) } - + document.addEventListener('keydown', captureKey) }) }) - + + return themeManager +} + +function initializeGuide() { + const guide = new Guide() + + guide.loadSteps() + guide.loadConfig() + guide.start() + + return guide +} + +function addEventListeners(calculator, themeManager) { + document.querySelector('.reset').addEventListener('click', () => calculator.handleResetValue()) + document.querySelector('.remove-last').addEventListener('click', () => calculator.handleRemoveLastInput()) + document.querySelector('.calculate').addEventListener('click', () => calculator.calculate()) + document.getElementById('reset-theme').addEventListener('click', () => themeManager.resetKeys()) + + document.querySelectorAll('.add-input').forEach(button => { + button.addEventListener('click', () => { + const value = button.dataset.operation || button.value + calculator.handleAddInput(value) + }) + }) + document.addEventListener('keydown', (event) => { const keyActions = calculator.getKeyboardActions() + if (keyActions[event.key]) { keyActions[event.key](event) } else if ('0123456789+-*/.'.includes(event.key)) { @@ -70,8 +77,4 @@ document.addEventListener('DOMContentLoaded', () => { const themeId = themeManager.getThemeIdByKey(event.key) if (themeId) themeManager.changeThemeById(themeId) }) - - document.getElementById('reset-theme').addEventListener('click', function() { - themeManager.resetKeys() - }) -}) +} diff --git a/src/js/speak.js b/src/js/speak.js new file mode 100644 index 0000000..baa3cd1 --- /dev/null +++ b/src/js/speak.js @@ -0,0 +1,93 @@ +export class Speak { + #recognition + #startBtn + #isListening = false + #microphoneIcon + #microphoneTooltip + #calculator + + constructor( + calculator, + startBtn = document.getElementById('startVoice'), + microphoneIcon = document.getElementById('microphoneIcon'), + microphoneTooltip = document.getElementById('microphoneTooltip') + ) { + this.#calculator = calculator + this.#startBtn = startBtn + this.#microphoneIcon = microphoneIcon + this.#microphoneTooltip = microphoneTooltip + this.setup() + } + + setup() { + if (!('webkitSpeechRecognition' in window)) { + this.#startBtn.disabled = true + console.error('Speech recognition not available') + return + } + + this.#recognition = new webkitSpeechRecognition() + this.addListeners() + } + + start() { + this.#calculator.setDisplayValue('') + this.#recognition.continuous = true + this.#recognition.lang = 'pt-BR' + this.#recognition.interimResults = true + + this.#recognition.onresult = (event) => { + for (const result of Object.values(event.results)) { + const { transcript } = result[0] + const { isFinal } = result + + if (isFinal) this.#calculator.executeVoiceCommand(transcript.trim(), this.getVoiceActions()) + } + } + + this.#recognition.onend = () => { + this.updateListeningState(false) + } + + this.#recognition.start() + this.updateListeningState(true) + } + + stop() { + this.#recognition.stop() + this.updateListeningState(false) + } + + toggleListening() { + if (this.#isListening) return this.stop() + + this.start() + } + + updateListeningState(isListening) { + this.#isListening = isListening + + if (isListening) { + this.#microphoneIcon.classList.remove('fa-microphone-slash') + this.#microphoneIcon.classList.add('fa-microphone') + this.#microphoneTooltip.textContent = 'Desativar microfone' + return + } + + this.#microphoneIcon.classList.remove('fa-microphone') + this.#microphoneIcon.classList.add('fa-microphone-slash') + this.#microphoneTooltip.textContent = 'Ativar microfone' + } + + addListeners() { + this.#startBtn.addEventListener('click', () => this.toggleListening()) + } + + getVoiceActions() { + return { + 'calcular': () => this.#calculator.calculate(), + 'apagar': () => this.#calculator.handleRemoveLastInput(), + 'limpar': () => this.#calculator.handleResetValue() + } + } +} diff --git a/src/js/themeManager.js b/src/js/themeManager.js index 3bb8bc8..4d4d1a8 100644 --- a/src/js/themeManager.js +++ b/src/js/themeManager.js @@ -1,19 +1,28 @@ export class ThemeManager { - constructor(themesConfig, btnTheme, keySelectors, root = document.querySelector(':root')) { + #btnTheme + #keySelectors + #root + + constructor( + themesConfig, + btnTheme = document.getElementById('btnTheme'), + keySelectors = document.querySelectorAll('.key-selector'), + root = document.querySelector(':root') + ) { this.themesConfig = themesConfig - this.btnTheme = btnTheme - this.keySelectors = [...keySelectors] - this.root = root + this.#btnTheme = btnTheme + this.#keySelectors = [...keySelectors] + this.#root = root } applyTheme(themeColors) { themeColors.forEach(({ name, value }) => { - this.root.style.setProperty(name, value) + this.#root.style.setProperty(name, value) }) } applyStoredKeys() { - this.keySelectors.forEach(selector => { + this.#keySelectors.forEach(selector => { const key = localStorage.getItem(selector.getAttribute('data-key')) if (key) this.setKeyForTheme(selector, key) }) @@ -23,20 +32,20 @@ export class ThemeManager { const theme = Object.values(this.themesConfig).find(theme => theme.id === themeId) if (!theme) return - this.btnTheme.value = theme.id + this.#btnTheme.value = theme.id this.applyTheme(theme.colors) } setPreferColorSchemeTheme() { - if (!this.btnTheme || !window.matchMedia) return + if (!this.#btnTheme || !window.matchMedia) return Object.keys(this.themesConfig).forEach(themeName => { const matchedPreferredScheme = window.matchMedia(`(prefers-color-scheme: ${themeName})`)?.matches - if (matchedPreferredScheme) this.btnTheme.value = this.themesConfig[themeName].id + if (matchedPreferredScheme) this.#btnTheme.value = this.themesConfig[themeName].id }) - this.changeThemeById(this.btnTheme.value) + this.changeThemeById(this.#btnTheme.value) } setKeyForTheme(selector, key) { @@ -46,7 +55,7 @@ export class ThemeManager { } resetKeys() { - this.keySelectors.forEach(selector => { + this.#keySelectors.forEach(selector => { selector.removeAttribute('data-chosen-key') selector.textContent = selector.getAttribute('data-key') localStorage.removeItem(selector.getAttribute('data-key')) @@ -54,8 +63,16 @@ export class ThemeManager { } getThemeIdByKey(key) { - const themeId = this.keySelectors.find(selector => selector.getAttribute('data-chosen-key') === key)?.getAttribute('data-key') + const themeId = this.#keySelectors.find(selector => selector.getAttribute('data-chosen-key') === key)?.getAttribute('data-key') return themeId } + + getBtnTheme() { + return this.#btnTheme + } + + getKeySelectors() { + return this.#keySelectors + } }