Skip to content

Commit

Permalink
Merge pull request #8 from Igorcbraz/Web-Speech
Browse files Browse the repository at this point in the history
Web speech
  • Loading branch information
Igorcbraz authored Nov 27, 2024
2 parents 2712b6c + 6d167f8 commit 34e5afa
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 59 deletions.
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js@8.0.0-beta.1/minified/introjs.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="./src/css/index.css">
</head>
<body>
Expand Down Expand Up @@ -83,6 +84,11 @@ <h4 class="col-3" id="reset-theme">theme</h4>
</div>
</div>
</div>

<button id="startVoice" class="floating-btn">
<i class="fas fa-microphone-slash" id="microphoneIcon"></i>
<span id="microphoneTooltip" class="tooltip">Ativar microfone</span>
</button>

<script src="https://cdn.jsdelivr.net/npm/intro.js@8.0.0-beta.1/minified/intro.min.js"></script>
<script type="module" src="./src/js/main.js"></script>
Expand Down
54 changes: 53 additions & 1 deletion src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
--background-dark : #232c43;
--background-very-dark: #182034;


--key-color-top : #ffffff;
--key-color-bottom : #3a4764;
--key-background : #eae3dc;
Expand Down Expand Up @@ -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;
}
13 changes: 12 additions & 1 deletion src/js/calculator.js
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -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]()
}
}
21 changes: 18 additions & 3 deletions src/js/guide.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class Guide {
doneLabel: 'Finalizar',
}
}

loadSteps() {
this.#config.steps = [
{
Expand Down Expand Up @@ -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: `
<p>Clique no botão para ativar os comandos de voz. Você pode usar os seguintes comandos:</p>
<ul>
<li><strong>calcular</strong>: Calcular a conta.</li>
<li><strong>apagar</strong>: Apagar o último dígito.</li>
<li><strong>limpar</strong>: Limpar a expressão.</li>
</ul>
<p>Além de ditar a conta que deseja calcular, como "4 mais 4".</p>
`,
tooltipClass: 'custom-tooltip',
highlightClass: 'custom-highlight'
}
]
}

loadConfig () {
loadConfig() {
this.#intro.setOptions(this.#config)
}

start () {
start() {
this.#intro.start()
}
}
87 changes: 45 additions & 42 deletions src/js/main.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,73 @@
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()

const captureKey = (event) => {
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)) {
Expand All @@ -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()
})
})
}
93 changes: 93 additions & 0 deletions src/js/speak.js
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Loading

0 comments on commit 34e5afa

Please sign in to comment.