diff --git a/package.json b/package.json index e6ae3051..987affd7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "date-fns": "^2.16.1", "debounce": "^1.2.0", "debug": "4.3.1", - "electron-context-menu": "2.3.0", "electron-devtools-installer": "3.2.0", "electron-squirrel-startup": "1.0.0", "electron-window-state": "5.0.3", diff --git a/src/interfaces.ts b/src/interfaces.ts index 744601bb..5a15f421 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -170,3 +170,12 @@ export interface RootState { releaseChannelOverride: string; }; } + +/** + * Context menu actions that can be taken on a specific log line. + */ +export enum LogLineContextMenuActions { + SHOW_IN_CONTEXT, + COPY_TO_CLIPBOARD, + OPEN_SOURCE, +} diff --git a/src/ipc-events.ts b/src/ipc-events.ts index 6e489945..baa4c861 100644 --- a/src/ipc-events.ts +++ b/src/ipc-events.ts @@ -24,4 +24,5 @@ export const enum IpcEvents { TOGGLE_FILTER = 'TOGGLE_FILTER', COPY = 'COPY', RESET = 'RESET', + OPEN_LOG_CONTEXT_MENU = 'OPEN_LOG_CONTEXT_MENU', } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f29c2873..b4ef5859 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -6,6 +6,8 @@ import { dialog, systemPreferences, IpcMainEvent, + Menu, + MenuItemConstructorOptions, } from 'electron'; import * as path from 'path'; @@ -13,6 +15,7 @@ import { settingsFileManager } from './settings'; import { changeIcon } from './app-icon'; import { ICON_NAMES } from '../shared-constants'; import { IpcEvents } from '../ipc-events'; +import { LogLineContextMenuActions, LogType } from '../interfaces'; export class IpcManager { constructor() { @@ -27,6 +30,7 @@ export class IpcManager { this.setupQuit(); this.setupOpenRecent(); this.setupTitleBarClickMac(); + this.setupContextMenus(); } public openFile(pathName: string) { @@ -123,24 +127,12 @@ export class IpcManager { } private setupGetPath() { - type name = - | 'home' - | 'appData' - | 'userData' - | 'temp' - | 'exe' - | 'module' - | 'desktop' - | 'documents' - | 'downloads' - | 'music' - | 'pictures' - | 'videos' - | 'logs'; - - ipcMain.handle(IpcEvents.GET_PATH, (_event, pathName: name) => { - return app.getPath(pathName); - }); + ipcMain.handle( + IpcEvents.GET_PATH, + (_event, pathName: Parameters[0]) => { + return app.getPath(pathName); + }, + ); } private setupGetUserAgent() { @@ -202,6 +194,49 @@ export class IpcManager { app.addRecentDocument(filename); }); } + + private setupContextMenus() { + ipcMain.handle(IpcEvents.OPEN_LOG_CONTEXT_MENU, (event, type: LogType) => { + return new Promise((resolve) => { + const maybeShowInContext: MenuItemConstructorOptions[] = + type === LogType.BROWSER || type === LogType.WEBAPP + ? [ + { + type: 'separator', + }, + { + label: 'Show in "All Desktop Logs"', + click: () => { + resolve(LogLineContextMenuActions.SHOW_IN_CONTEXT); + }, + }, + ] + : []; + + const menu = Menu.buildFromTemplate([ + { + label: 'Copy Line', + click: () => { + resolve(LogLineContextMenuActions.COPY_TO_CLIPBOARD); + }, + }, + { + label: 'Show Line in Source', + click: () => { + resolve(LogLineContextMenuActions.OPEN_SOURCE); + }, + }, + ...maybeShowInContext, + ]); + menu.popup({ + window: BrowserWindow.fromWebContents(event.sender) || undefined, + callback: () => { + resolve(undefined); + }, + }); + }); + }); + } } export const ipcManager = new IpcManager(); diff --git a/src/main/menu.ts b/src/main/menu.ts index 7413fd28..67a7e741 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -251,9 +251,6 @@ export class AppMenu { * Actually creates the menu. */ public setupMenu() { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('electron-context-menu')(); - this.menu = getMenuTemplate({ openItems: this.getOpenItems(), pruneItems: this.getPruneItems(), diff --git a/src/renderer/components/log-line-details/details.tsx b/src/renderer/components/log-line-details/details.tsx index 36f79d2c..8cfd361a 100644 --- a/src/renderer/components/log-line-details/details.tsx +++ b/src/renderer/components/log-line-details/details.tsx @@ -1,19 +1,15 @@ import { observer } from 'mobx-react'; -import { exec } from 'child_process'; import { SleuthState } from '../../state/sleuth'; import React from 'react'; import classNames from 'classnames'; import { Card, Button, ButtonGroup, Tag, Elevation } from '@blueprintjs/core'; -import debug from 'debug'; import { LogEntry } from '../../../interfaces'; import { LogLineData } from './data'; import { Timestamp } from './timestamp'; -import { shell } from 'electron'; import { getIsBookmark, toggleBookmark } from '../../state/bookmarks'; import { capitalize } from '../../../utils/capitalize'; - -const d = debug('sleuth:details'); +import { openLineInSource } from '../../../utils/open-line-in-source'; export interface LogLineDetailsProps { state: SleuthState; @@ -52,20 +48,9 @@ export class LogLineDetails extends React.Component< if (selectedEntry && selectedEntry.sourceFile) { const { sourceFile, line } = selectedEntry; - if (defaultEditor) { - const cmd = defaultEditor - .replace('{filepath}', `"${sourceFile}"`) - .replace('{line}', line.toString(10)); - - d(`Executing ${cmd}`); - exec(cmd, (error: Error) => { - if (!error) return; - d(`Tried to open source file, but failed`, error); - shell.showItemInFolder(sourceFile); - }); - } else { - shell.showItemInFolder(sourceFile); - } + openLineInSource(line, sourceFile, { + defaultEditor, + }); } } diff --git a/src/renderer/components/log-table.tsx b/src/renderer/components/log-table.tsx index 2dcbc097..7fb6952d 100644 --- a/src/renderer/components/log-table.tsx +++ b/src/renderer/components/log-table.tsx @@ -21,6 +21,8 @@ import { DateRange, LogType, ProcessableLogType, + LogLineContextMenuActions, + LogFile, } from '../../interfaces'; import { didFilterChange } from '../../utils/did-filter-change'; import { isReduxAction } from '../../utils/is-redux-action'; @@ -38,6 +40,10 @@ import { RepeatedLevels } from '../../shared-constants'; import { reaction } from 'mobx'; import { Tag } from 'antd'; import { observer } from 'mobx-react'; +import { showLogLineContextMenu } from '../ipc'; +import { clipboard } from 'electron'; +import { openLineInSource } from '../../utils/open-line-in-source'; +import { getCopyText } from '../state/copy'; const d = debug('sleuth:logtable'); @@ -268,6 +274,40 @@ export class LogTable extends React.Component { }); } + /** + * Show a context menu for the individual log lines in the table + */ + private async onRowRightClick(params: RowMouseEventHandlerParams) { + const rowData: LogEntry = params.rowData; + // type assertion because this component should only appear when you have a LogFile showing + const logType = (this.props.state.selectedLogFile as LogFile).logType; + const response = await showLogLineContextMenu(logType); + + switch (response) { + case LogLineContextMenuActions.COPY_TO_CLIPBOARD: { + const copyText = getCopyText(rowData); + clipboard.writeText(copyText); + break; + } + case LogLineContextMenuActions.OPEN_SOURCE: { + const { line, sourceFile } = rowData; + openLineInSource(line, sourceFile, { + defaultEditor: this.props.state.defaultEditor, + }); + break; + } + case LogLineContextMenuActions.SHOW_IN_CONTEXT: + { + this.props.state.selectLogFile(null, LogType.ALL); + const matchingIndex = this.state.sortedList.findIndex( + (row) => row.momentValue === rowData.momentValue, + ); + this.changeSelection(matchingIndex); + } + break; + } + } + /** * Handles the change of sorting direction. This method is passed to the LogTableHeaderCell * components, who call it once the user changes sorting. @@ -639,6 +679,7 @@ export class LogTable extends React.Component { rowGetter: this.rowGetter, rowCount: sortedList.length, onRowClick: this.onRowClick, + onRowRightClick: this.onRowRightClick, rowClassName: this.rowClassNameGetter, headerHeight: 30, sort: this.onSortChange, diff --git a/src/renderer/ipc.ts b/src/renderer/ipc.ts index 3649d909..5ace9d7f 100644 --- a/src/renderer/ipc.ts +++ b/src/renderer/ipc.ts @@ -1,26 +1,14 @@ -import { ipcRenderer } from 'electron'; +import { ipcRenderer, app } from 'electron'; import { ICON_NAMES } from '../shared-constants'; import { IpcEvents } from '../ipc-events'; +import { LogLineContextMenuActions, LogType } from '../interfaces'; // This file handles sending IPC events. Other classes might // listen to IPC events. -type name = - | 'home' - | 'appData' - | 'userData' - | 'cache' - | 'temp' - | 'exe' - | 'module' - | 'desktop' - | 'documents' - | 'downloads' - | 'music' - | 'pictures' - | 'videos' - | 'logs'; -export function getPath(path: name): Promise { +export function getPath( + path: Parameters[0], +): Promise { return ipcRenderer.invoke(IpcEvents.GET_PATH, path); } @@ -55,3 +43,9 @@ export function showMessageBox( export function changeIcon(iconName: ICON_NAMES) { return ipcRenderer.invoke(IpcEvents.CHANGE_ICON, iconName); } + +export function showLogLineContextMenu( + type: LogType, +): Promise { + return ipcRenderer.invoke(IpcEvents.OPEN_LOG_CONTEXT_MENU, type); +} diff --git a/src/renderer/state/copy.ts b/src/renderer/state/copy.ts index ada52b95..5cdafb20 100644 --- a/src/renderer/state/copy.ts +++ b/src/renderer/state/copy.ts @@ -31,7 +31,7 @@ export function copy(state: SleuthState): boolean { return false; } -function getCopyText(entry: LogEntry) { +export function getCopyText(entry: LogEntry) { const { message, meta } = entry; let { timestamp } = entry; diff --git a/src/utils/open-line-in-source.ts b/src/utils/open-line-in-source.ts new file mode 100644 index 00000000..c9a8c246 --- /dev/null +++ b/src/utils/open-line-in-source.ts @@ -0,0 +1,30 @@ +import { shell } from 'electron'; +import { exec } from 'child_process'; +import debug from 'debug'; + +const d = debug('sleuth:open-line-in-source'); + +interface OpenSourceOptions { + defaultEditor: string; +} + +export function openLineInSource( + line: number, + sourceFile: string, + options: OpenSourceOptions, +) { + if (options.defaultEditor) { + const cmd = options.defaultEditor + .replace('{filepath}', `"${sourceFile}"`) + .replace('{line}', line.toString(10)); + + d(`Executing ${cmd}`); + exec(cmd, (error: Error) => { + if (!error) return; + d(`Tried to open source file, but failed`, error); + shell.showItemInFolder(sourceFile); + }); + } else { + shell.showItemInFolder(sourceFile); + } +} diff --git a/yarn.lock b/yarn.lock index 7f3b4bc9..3af10378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4553,7 +4553,7 @@ cli-spinners@^2.4.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== -cli-truncate@^2.0.0, cli-truncate@^2.1.0: +cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== @@ -5520,15 +5520,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-context-menu@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-2.3.0.tgz#d9af9733b19a76c78c413d8eb7b00b1ae1ead356" - integrity sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw== - dependencies: - cli-truncate "^2.0.0" - electron-dl "^3.0.0" - electron-is-dev "^1.0.1" - electron-devtools-installer@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0" @@ -5539,15 +5530,6 @@ electron-devtools-installer@3.2.0: tslib "^2.1.0" unzip-crx-3 "^0.2.0" -electron-dl@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-3.0.2.tgz#302a46f9a449ddce720cb8e7f2a24c386e19a26c" - integrity sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g== - dependencies: - ext-name "^5.0.0" - pupa "^2.0.1" - unused-filename "^2.1.0" - electron-installer-common@^0.10.2: version "0.10.3" resolved "https://registry.yarnpkg.com/electron-installer-common/-/electron-installer-common-0.10.3.tgz#40f9db644ca60eb28673d545b67ee0113aef4444" @@ -5597,11 +5579,6 @@ electron-is-dev@^0.3.0: resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe" integrity sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4= -electron-is-dev@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.2.0.tgz#2e5cea0a1b3ccf1c86f577cee77363ef55deb05e" - integrity sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw== - electron-squirrel-startup@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz#19b4e55933fa0ef8f556784b9c660f772546a0b8" @@ -5866,11 +5843,6 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -6159,21 +6131,6 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -ext-list@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" - integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== - dependencies: - mime-db "^1.28.0" - -ext-name@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" - integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== - dependencies: - ext-list "^2.0.0" - sort-keys-length "^1.0.0" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -7598,11 +7555,6 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -8883,11 +8835,6 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-db@^1.28.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - mime-types@^2.1.12, mime-types@^2.1.25, mime-types@~2.1.19: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -9055,11 +9002,6 @@ mobx@5.15.7: resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665" integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw== -modify-filename@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1" - integrity sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE= - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -10489,13 +10431,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pupa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" - integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== - dependencies: - escape-goat "^2.0.0" - pure-rand@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" @@ -11899,20 +11834,6 @@ socks@^2.6.2: ip "^2.0.0" smart-buffer "^4.2.0" -sort-keys-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" - integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= - dependencies: - sort-keys "^1.0.0" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= - dependencies: - is-plain-obj "^1.0.0" - source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -12890,14 +12811,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -unused-filename@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-2.1.0.tgz#33719c4e8d9644f32d2dec1bc8525c6aaeb4ba51" - integrity sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg== - dependencies: - modify-filename "^1.1.0" - path-exists "^4.0.0" - unzip-crx-3@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz#d5324147b104a8aed9ae8639c95521f6f7cda292"