diff --git a/CHANGELOG.md b/CHANGELOG.md index 3988d36..7637324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,53 +1,58 @@ -### 1.2.0 - 2022-09-24 - Always Listening - -- Added a toggle-able `Always Listening` mode - - Each command must be prefixed with an activation phrase (defaulted to `okay ghost`) -- Added fix for `maxPower` command (didn't work in normal DIM, only in beta) -- Added fix for search already being populated when performing a command. - -### 1.1.3 - 2022-09-20 - Perk fix - -- Fixed perk matching - -### 1.1.2 - 2022-09-19 - Options page links - -- Added an `onInstalled` event for the extension. It'll direct users to the options page. -- Link to the options page from the `?` - -### 1.1.1 - 2022-09-16 - Better performance - -- Use a `waitForElement/waitForSearchToUpdate` function instead of arbitrarily sleeping in code - - Results in at least a 300% improvement in action duration - -### 1.1.0 - 2022-09-13 - The One with Custom Commands - -- Quite a few changes in this one -- Reworked how the extension waits for the search to update visible (not dimmed) items - - Really reworked how all UI interactions are performed -- Added the ability to customize the command words for particular actions - - Visit the options page (click the extension icon) to set these -- Added a `store` command for storing items in the vault -- Added an `equip` command for directly equipping an item on your current character (when possible) - - This works like how `transfer` previously did, and `transfer` has been updated to only transfer, not equip -- (from above) `Transfer` now only transfers instead of equipping -- Added mic icon on page with a link to the website - - When listening, there will be text next to the icon that updates as the user speaks their command -- Added link to the [Voice DIM website](https://www.voicedim.com) via the `?` icon. -- Fixed `Start Farming mode` command if a user has at least 10 loadouts saved - -### 1.0.1 - 2022-08-23 - Corrected Shortcut - -- Defaulted the shortcut correctly (set to `Ctrl+Shift+0`) - -### 1.0.0 - 2022-08-19 - Initial Release - -- See [Reddit post](https://www.reddit.com/r/DestinyTheGame/comments/wseigx/interact_with_dim_using_your_voice/) about the available commands with more info - - Commands are available to: - - Transfer a weapon by name - - Transfer a weapon with particular perks - - Transfer a weapon by attribute (energy type, slot, ammo type, etc) - - Collect from the postmaster - - Start/Stop farming mode - - Equip loadouts by name - - Equip max power -- Use global shortcut to activate listening +### 1.2.1 - 2022-10-03 - Only in Inventory + +- Added fix for loading DIM on another tab. Previously required reloading the page on the inventory tab. Now Voice DIM will load no matter what page is started on +- Added fix for a user having the item popup sidebar being collapsed. + +### 1.2.0 - 2022-09-24 - Always Listening + +- Added a toggle-able `Always Listening` mode + - Each command must be prefixed with an activation phrase (defaulted to `okay ghost`) +- Added fix for `maxPower` command (didn't work in normal DIM, only in beta) +- Added fix for search already being populated when performing a command. + +### 1.1.3 - 2022-09-20 - Perk fix + +- Fixed perk matching + +### 1.1.2 - 2022-09-19 - Options page links + +- Added an `onInstalled` event for the extension. It'll direct users to the options page. +- Link to the options page from the `?` + +### 1.1.1 - 2022-09-16 - Better performance + +- Use a `waitForElement/waitForSearchToUpdate` function instead of arbitrarily sleeping in code + - Results in at least a 300% improvement in action duration + +### 1.1.0 - 2022-09-13 - The One with Custom Commands + +- Quite a few changes in this one +- Reworked how the extension waits for the search to update visible (not dimmed) items + - Really reworked how all UI interactions are performed +- Added the ability to customize the command words for particular actions + - Visit the options page (click the extension icon) to set these +- Added a `store` command for storing items in the vault +- Added an `equip` command for directly equipping an item on your current character (when possible) + - This works like how `transfer` previously did, and `transfer` has been updated to only transfer, not equip +- (from above) `Transfer` now only transfers instead of equipping +- Added mic icon on page with a link to the website + - When listening, there will be text next to the icon that updates as the user speaks their command +- Added link to the [Voice DIM website](https://www.voicedim.com) via the `?` icon. +- Fixed `Start Farming mode` command if a user has at least 10 loadouts saved + +### 1.0.1 - 2022-08-23 - Corrected Shortcut + +- Defaulted the shortcut correctly (set to `Ctrl+Shift+0`) + +### 1.0.0 - 2022-08-19 - Initial Release + +- See [Reddit post](https://www.reddit.com/r/DestinyTheGame/comments/wseigx/interact_with_dim_using_your_voice/) about the available commands with more info + - Commands are available to: + - Transfer a weapon by name + - Transfer a weapon with particular perks + - Transfer a weapon by attribute (energy type, slot, ammo type, etc) + - Collect from the postmaster + - Start/Stop farming mode + - Equip loadouts by name + - Equip max power +- Use global shortcut to activate listening diff --git a/package.json b/package.json index b9b279a..4aaa6a8 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "voice-dim", - "version": "1.2.0", + "version": "1.2.1", "description": "Perform common DIM actions by using speech recognition.", "main": "dist/chrome/js/voice-dim.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "zip": "npm run build && python scripts/build.py", "build": "rimraf dist/ && webpack --env mode=production --config webpack.config.js", - "watch": "webpack --env mode=development --config webpack.config.js --progress --watch" + "watch": "webpack --env mode=development --config webpack.config.js --progress --watch", + "update-version": "python scripts/update_version.py" }, "repository": { "type": "git", @@ -45,4 +46,4 @@ "webpack-cli": "^4.10.0", "webpack-visualizer-plugin2": "^1.0.0" } -} +} \ No newline at end of file diff --git a/public/manifest.chrome.json b/public/manifest.chrome.json index 45f0d48..059316b 100644 --- a/public/manifest.chrome.json +++ b/public/manifest.chrome.json @@ -1,14 +1,14 @@ { "name": "Voice DIM", "description": "Control DIM with your voice.", - "version": "1.2.0", + "version": "1.2.1", "manifest_version": 3, "background": { "service_worker": "js/background.js" }, "content_scripts": [ { - "matches": ["https://*.destinyitemmanager.com/*inventory"], + "matches": ["https://*.destinyitemmanager.com/*"], "js": ["js/voiceDim.js"], "css": ["css/voiceDim.css"] } diff --git a/public/manifest.firefox.json b/public/manifest.firefox.json index 18bfbed..79fe09c 100644 --- a/public/manifest.firefox.json +++ b/public/manifest.firefox.json @@ -1,7 +1,7 @@ { "name": "Voice DIM", "description": "Control DIM with your voice.", - "version": "1.2.0", + "version": "1.2.1", "manifest_version": 2, "background": { "scripts": ["js/background.js"] diff --git a/scripts/update_version.py b/scripts/update_version.py index f21b13b..fc18959 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -1,3 +1,4 @@ +from datetime import datetime import getopt import json import os @@ -37,31 +38,54 @@ def get_next_version(current_version: str, part_to_update: str): return f"{major_version}.{minor_version}.{bugfix_version}" -def write_new_version(file_path, new_version): +def write_new_version(file_path: str, new_version: str, dry_run: bool = False): with open(file_path, 'r') as f: data = json.load(f) data['version'] = f"{new_version}" + if dry_run: + return + with open(file_path, 'w') as f: - json.dump(data, f, indent=4) + json.dump(data, f, indent=2) + + +def write_changelog_update(file_path: str, new_version: str, dry_run: bool = False): + with open(file_path, 'r') as f: + data = f.read() + + if data.find(new_version) < 0: + now = datetime.now() + date = now.strftime('%Y-%m-%d') + + data = f"### {new_version} - {date} - \n\n- <insert changes>\n\n{data}" + print(data) + + if dry_run: + return + + with open(file_path, 'w') as f: + f.write(data) def main(): argument_list = sys.argv[1:] - options = 'p:' - long_options = "part=" + options = 'dp:' + long_options = ["dry-run", "part="] # Parsing argument arguments, values = getopt.getopt(argument_list, options, long_options) part = 'bugfix' + dry_run = False # checking each argument for current_arg, current_value in arguments: - if current_arg in ("-p", "--part"): if current_value in ('major', 'minor', 'bugfix'): part = current_value else: print("No part provided. Updating the bugfix version by default") + if current_arg in ('-d', '--dry-run'): + dry_run = True git_root = get_git_root(os.getcwd()) files = [git_root + '/public/manifest.chrome.json', git_root + @@ -86,7 +110,9 @@ def main(): print(next_version) for file in files: - write_new_version(file, next_version) + write_new_version(file, next_version, dry_run) + + write_changelog_update(git_root+"/CHANGELOG.md", next_version, dry_run) if __name__ == "__main__": diff --git a/src/ts/background.ts b/src/ts/background.ts index 1845280..db5c4a6 100644 --- a/src/ts/background.ts +++ b/src/ts/background.ts @@ -2,18 +2,36 @@ import { infoLog } from './common'; chrome.commands.onCommand.addListener((command: any) => { infoLog('voice dim', `Command "${command}" triggered`); + sendDimTabMessage({ dimShortcutPressed: true }); +}); - chrome.tabs.query({}, (tabs: any[]) => { - const dimTabs = tabs.filter((tab: { url: string }) => tab.url?.match(/destinyitemmanager\.com.*inventory/)); - - if (dimTabs && dimTabs[0]?.id) - chrome.tabs.sendMessage(dimTabs[0].id, { dimShortcutPressed: true }, (response: any) => { - infoLog('voice dim', { response }); - }); - }); +chrome.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) { + const dimTabId = await getDimTabId(); + if (dimTabId && tabId === dimTabId) { + if (changeInfo.url && !changeInfo.url.includes('inventory')) { + sendDimTabMessage('not on inventory page'); + } else if (changeInfo.url && changeInfo.url.includes('inventory')) { + sendDimTabMessage('on inventory page'); + } + } }); -chrome.runtime.onMessage.addListener((data: any) => { +async function getDimTabId(): Promise<number | undefined | null> { + const dimTabs = await chrome.tabs.query({ url: 'https://*.destinyitemmanager.com/*' }); + return dimTabs && dimTabs.length >= 1 ? dimTabs[0]?.id : null; +} +async function sendDimTabMessage(message: any) { + const dimTabId = await getDimTabId(); + if (dimTabId) { + infoLog('message', 'sending', message); + + chrome.tabs.sendMessage(dimTabId, message, (response: any) => { + infoLog('voice dim', { response }); + }); + } +} + +chrome.runtime.onMessage.addListener((data: any, sender: chrome.runtime.MessageSender) => { infoLog('voice dim', { data }); if (data === 'showOptions') { openOptionsPage(); diff --git a/src/ts/common.ts b/src/ts/common.ts index 767fd46..fe22474 100644 --- a/src/ts/common.ts +++ b/src/ts/common.ts @@ -1,12 +1,11 @@ -export interface Action { - func: () => void; - timeout: number; -} - export function infoLog(tag: string, message: unknown, ...args: unknown[]) { console.log(`[${tag}]`, message, ...args); } +export function debugLog(tag: string, message: unknown, ...args: unknown[]) { + console.debug(`[${tag}]`, message, ...args); +} + export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(func: F, waitFor: number = 300) => { @@ -54,7 +53,7 @@ export async function waitForElementToDisplay( checkFrequencyInMs: number = 50, timeoutInMs: number = 2000 ): Promise<Element | null> { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { var startTimeInMs = Date.now(); (function loopSearch() { if (document.querySelector(selector) != null) { @@ -62,8 +61,8 @@ export async function waitForElementToDisplay( } else { setTimeout(function () { if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs) { - infoLog('voice dim', "couldn't find", selector); - return; + debugLog('voice dim', "couldn't find", selector); + return reject(); } loopSearch(); }, checkFrequencyInMs); @@ -93,13 +92,13 @@ export const DEFAULT_ALWAYS_LISTENING: AlwaysListening = { activationPhrase: 'okay ghost', }; -export function store(key: string, value: any) { +export function store<T>(key: string, value: T) { chrome.storage.local.set({ [key]: value }, () => { infoLog('voice dim', 'Stored', key, value); }); } -export function retrieve(key: string, defaultValue: any): Promise<any> { +export function retrieve<T>(key: string, defaultValue: T): Promise<T> { return new Promise((resolve, reject) => { chrome.storage.local.get([key], function (result) { if (chrome.runtime.lastError) { diff --git a/src/ts/options.ts b/src/ts/options.ts index b29060d..06a5936 100644 --- a/src/ts/options.ts +++ b/src/ts/options.ts @@ -13,7 +13,7 @@ function onCommandChange() { Object.keys(DEFAULT_COMMANDS).forEach((command) => { commands[command] = getTextValueById(command); }); - store('commands', commands); + store<Record<string, string[]>>('commands', commands); updateSaveText(true, 'Saved!'); setTimeout(() => updateSaveText(false), 3000); @@ -46,7 +46,10 @@ function onActivationPhraseChange() { updateSaveText(true, 'Saved!'); setTimeout(() => updateSaveText(false), 3000); - store('alwaysListening', { active: listeningToggle.checked, activationPhrase: activationPhrase.value }); + store<AlwaysListening>('alwaysListening', { + active: listeningToggle.checked, + activationPhrase: activationPhrase.value, + }); sendListenOptionsMessage(); } @@ -56,7 +59,10 @@ function onAlwaysListeningChange(listeningOptions: AlwaysListening) { updateSaveText(true, 'Saved!'); setTimeout(() => updateSaveText(false), 3000); - store('alwaysListening', { active: listeningOptions.active, activationPhrase: listeningOptions.activationPhrase }); + store<AlwaysListening>('alwaysListening', { + active: listeningOptions.active, + activationPhrase: listeningOptions.activationPhrase, + }); sendListenOptionsMessage(); } diff --git a/src/ts/voiceDim.ts b/src/ts/voiceDim.ts index 9d159fa..e253529 100644 --- a/src/ts/voiceDim.ts +++ b/src/ts/voiceDim.ts @@ -1,6 +1,7 @@ import Fuse from 'fuse.js'; import { AlwaysListening, + debugLog, DEFAULT_ALWAYS_LISTENING, DEFAULT_COMMANDS, getVisibleItems, @@ -197,7 +198,6 @@ async function parseSpeech(this: any, transcript: string) { infoLog('voice dim', "Couldn't determine correct action"); return; } - infoLog('voice dim', 'Action:', closestAction.match); query = getNewQuery(query, closestMatch.match); await potentialActions[closestAction.match].call(this, query, closestAction.match); @@ -242,7 +242,7 @@ function getCurrentCharacterClass(): string { async function handleItemMovement(query: string, action: string): Promise<void> { infoLog('voice dim', 'in handleItemMovement', { query, action }); const itemToMove = await getItemToMove(query); - infoLog('voice dim', { itemToMove }); + debugLog('voice dim', { itemToMove }); if (!itemToMove) { await clearSearchBar(); return; @@ -286,7 +286,11 @@ async function getItemToMove(query: string): Promise<Element | null> { async function transferItem(item: Element) { item.dispatchEvent(uiEvents.singleClick); const currentClass = getCurrentCharacterClass(); - const storeDiv = await waitForElementToDisplay(`[title^="Store"] [data-icon*="${currentClass}"]`); + const expandCollapseButton = await waitForElementToDisplay('div[title^="Expand or collapse"]'); + if (!document.querySelector('div[class^="ItemMoveLocations"]')) { + expandCollapseButton?.dispatchEvent(uiEvents.singleClick); + } + const storeDiv = await waitForElementToDisplay(`[title^="Store"] [data-icon*="${currentClass}"]`, 500); storeDiv?.dispatchEvent(uiEvents.singleClick); } @@ -432,18 +436,18 @@ function getClosestMatch(availableItems: string[], query: string): FuseMatch | n }; const fuse = new Fuse(availableItems, options); const result = fuse.search(query); - infoLog('voice dim', { result, query }); + debugLog('voice dim', { result, query }); if (isAcceptableResult(result)) { return { toReplace: query, match: result[0].item }; } - infoLog('voice dim', "Couldn't find a match. Trying to find match by splitting the current query."); + debugLog('voice dim', "Couldn't find a match. Trying to find match by splitting the current query."); const splitQuery = query.split(' '); for (const split of splitQuery) { const splitResult = fuse.search(split); - infoLog('voice dim', { splitResult, split }); + debugLog('voice dim', { splitResult, split }); return isAcceptableResult(splitResult) ? { toReplace: split, match: splitResult[0].item } : { toReplace: '', match: '' }; @@ -478,7 +482,6 @@ async function clearSearchBar() { const clearButton = document.querySelector('.filter-bar-button[title^=Clear]'); const initialCount = getVisibleItems().length; let waitForUpdate = false; - console.log(clearButton); clearButton?.dispatchEvent(uiEvents.singleClick); if (searchBar && searchBar?.value !== '') { searchBar.value = ''; @@ -498,8 +501,9 @@ function handleShortcutPress() { function initializeShortcutListening() { annyang.addCallback('result', (userSaid: string[]) => { - infoLog('shortcut', userSaid); + debugLog('shortcut', userSaid); const transcript = userSaid[0].trim().toLowerCase(); + infoLog('voice dim', 'Heard', transcript); updateUiTranscript(transcript, true); parseSpeech(transcript); annyang.abort(); @@ -510,7 +514,7 @@ function initializeShortcutListening() { function initializeAlwaysListening() { annyang.start({ autoRestart: listeningOptions.active, continuous: listeningOptions.active }); annyang.addCallback('result', (userSaid?: string[] | undefined) => { - infoLog('voice dim', { userSaid }); + debugLog('voice dim', { userSaid }); if (userSaid) { let actionPerformed = false; for (let said of userSaid) { @@ -524,7 +528,7 @@ function initializeAlwaysListening() { // include a space intentionally if (said.includes(`${phrase} `)) { const transcript = said.split(`${phrase} `)[1]; - infoLog('voice dim', { transcript }); + infoLog('voice dim', 'Heard', transcript); updateUiTranscript(transcript, true); parseSpeech(transcript); actionPerformed = true; @@ -539,19 +543,26 @@ function initializeAlwaysListening() { } chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { - infoLog('voice dim', { request }); + infoLog('voice dim', 'Message received', { request }); if (request.dimShortcutPressed && !listeningOptions.active) { - sendResponse({ ack: 'Acknowledged.' }); handleShortcutPress(); - return; } if (request === 'shortcut updated') { await getCustomCommands(); - sendResponse({ ack: 'Acknowledged.' }); } if (request === 'listening options updated') { await getAlwaysListeningOptions(); } + if (request === 'not on inventory page') { + infoLog('voice dim', 'no longer on inventory page'); + stopVoiceDim(); + } + if (request === 'on inventory page') { + infoLog('voice dim', 'on inventory page'); + init(); + } + sendResponse({ ack: 'Acknowledged.' }); + return true; }); async function getCustomCommands() { @@ -560,7 +571,7 @@ async function getCustomCommands() { infoLog('voice dim', { commands, mappedCommands }); } -function reverseMapCustomCommands(commands: any) { +function reverseMapCustomCommands(commands: Record<string, string[]>) { const newCommands: Record<string, string> = {}; for (const propName in commands) { const arr: Array<string> = commands[propName]; @@ -574,12 +585,22 @@ function reverseMapCustomCommands(commands: any) { async function getAlwaysListeningOptions() { listeningOptions = await retrieve('alwaysListening', DEFAULT_ALWAYS_LISTENING); infoLog('voice dim', { listeningOptions }); + startListening(); + // annyang.debug(true); +} + +function startListening() { annyang.abort(); - infoLog('voice dim', 'initializing annyang'); annyang.removeCallback(); - if (listeningOptions.active) initializeAlwaysListening(); - else initializeShortcutListening(); - // annyang.debug(true); + if (listeningOptions.active) { + initializeAlwaysListening(); + } else { + initializeShortcutListening(); + } +} + +function stopListening() { + annyang.abort(); } function createMicDiv() { @@ -628,11 +649,22 @@ async function getPerks() { } function init() { - getPerks(); - getCustomCommands(); - getAlwaysListeningOptions(); - createMicDiv(); - createHelpDiv(); + if (window.location.href.includes('inventory')) { + getPerks(); + getCustomCommands(); + getAlwaysListeningOptions(); + createMicDiv(); + createHelpDiv(); + } } window.addEventListener('load', init); + +function stopVoiceDim() { + const voiceDimDiv = document.getElementById('voiceDim'); + if (voiceDimDiv) voiceDimDiv.remove(); + const voiceDimHelp = document.getElementById('voiceDimHelp'); + if (voiceDimHelp) voiceDimHelp.remove(); + + stopListening(); +}