Este projeto foi criado como parte de um desafio, com o objetivo de desenvolver uma calculadora funcional e responsiva. Durante o processo, o escopo foi ampliado para incluir melhorias na arquitetura, acessibilidade e personalização, transformando o desafio em uma aplicação moderna e repleta de funcionalidades usando apenas html, css e javascript evitando ao máximo libs externas.
- Realizar operações matemáticas básicas (adição, subtração, multiplicação, divisão)
- Suporte a comandos de voz para facilitar a interação
- Temas personalizados para a interface (utilizando prefer-color-scheme)
- Responsividade para diferentes tamanhos de tela
- Integração com comandos via teclado
- Shortcuts dinâmicos para troca de temas
- Guia automático ensinando a utilizar a calculadora
Para desenvolver o recurso de suporte a comando de voz, sem adicionar bibliotecas externas ao projeto, foi utilizado o recurso
Web Speech API
Essa funcionalidade permite utilizar o recurso de áudio do próprio navegador e receber um ou mais resultados do que foi falado, em uma string.
No projeto, dentro da pasta JS temos o arquivo chamado Speak.js
com a implementação necessária para a utilização dos comandos de voz, nele primeiro iremos começar a utilizar a web speech api na função setup
:
if (!('webkitSpeechRecognition' in window)) {
this.#startBtn.disabled = true
console.error('Speech recognition not available')
return
}
this.#recognition = new webkitSpeechRecognition()
this.#recognition.continuous = true
this.#recognition.lang = 'pt-BR'
this.#recognition.interimResults = true
this.addListeners() // Adiciona o toggle de escutar ou não o usuário
Após a configuração, podemos começar a processar a fala e tomar algumas ações com o que foi dito, na função start
:
this.#recognition.onresult = (event) => {
const results = Object.values(event.results)
if (results.length) this.#startBtn.classList.add('listening')
for (const result of results) {
const { transcript } = result[0]
const { isFinal } = result
if (isFinal) this.#calculator.executeVoiceCommand(transcript.trim(), this.getVoiceActions())
}
}
this.#recognition.start()
Nesse caso, estamos iniciado o reconhecimento de fala e processando cada resultado (palavra ou frase dita) esperando até o momento da frase final ser completa, exemplo de resultados para uma frase "2+2":
- "dois" ou "2"
- "mais" ou "+"
- "dois" ou "2"
- ao terminar de falar a frase, entender que o resultado final deve ser "2+2"
No caso, é importante executar o comando de voz apenas com a frase final para ter a garantia do contexto como um todo foi processado e estamos obtendo o resultado mais próximo do desejado
Podemos também, lidar com o caso de erro:
this.#recognition.onend = () => {
this.updateListeningState(false)
}
A função updateListeningState, tem como objetivo alterar o estilo do botão de escuta.
Por fim, iremos precisar configurar uma função para parar de ouvir o usuário e manipular alguns estilos:
this.#recognition.stop()
this.updateListeningState(false)
this.#startBtn.classList.remove('listening')
Para auxiliar os 3 diferentes temas da calculadora, foi usado o recurso de mídia
prefer-color-scheme
.
Esse recurso possibilita o entendimento de qual a preferência do usuário em relação aos temas, assim podendo receber dois valores:
- Light (Claro)
- Dark (Escuro)
A maneira de aplicar esse recurso, no css, de acordo com a developer.mozilla é da seguinte maneira:
@media (prefers-color-scheme: dark) {
/* Configurações CSS para o tema dark */
}
@media (prefers-color-scheme: light) {
/* Configurações CSS para o tema light */
}
Mas com esse método o código irá ficar mais verboso e provavelmente com difícil manutenção, pois seria necessário repetir todas as propriedades desejadas com suas novas colorações.
Iremos apenas mudar os valores das variáveis do CSS com JS.
Primeiro iremos, na pasta de constantes, verificar o arquivo themes.js
:
export const themes = {
'Default': {
id: '1',
colors: [ { name: '--background', value: '#3a4764' } ]
},
'Light': {
id: '2',
colors: [ { name: '--background', value: '#e6e6e6' } ]
},
'Dark': {
id: '3',
colors: [ { name: '--background', value: '#17062a' } ]
}
}
Note o Nome dos temas e os id's utilizados, eles serão utilizados no arquivo ThemeManager.js
, na pasta JS, da seguinte forma:
function changeThemeById(themeId) {
const theme = Object.values(this.themesConfig).find(theme => theme.id === themeId)
if (!theme) return
this.#btnTheme.value = theme.id
this.applyTheme(theme.colors)
}
function setPreferColorSchemeTheme() {
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
})
this.changeThemeById(this.#btnTheme.value)
}
O objetivo aqui, está em fazer um código para troca de temas de forma versátil e de fácil manutenção.
Nesse caso, conseguimos buscar a preferência do usuário, através do window.matchMedia('(prefers-color-scheme: ${themeName})')
, concatenado com o nome do nosso tema (que é igual a nomenclatura desejada)
Após descobrir o tema desejado, podemos enviar o id do tema em questão para função changeThemeById
com objetivo de obter a nova coloração desejada
Para entender exatamente o que cada função está fazendo, como a
applyTheme
, acesse o arquivo na pasta JS
Com a ferramenta de devtools do seu navegador, acesse a aba "Rendering" e logo após procure o título "Emulate CSS media feature prefers-colors-scheme":
Alterando os valores padrões pelo devtools, o resultado final será: