electron#clipboard TypeScript Examples
The following examples show how to use
electron#clipboard.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: process.ts From DittoPlusPlus with MIT License | 6 votes |
readClipboardFiles = () => {
if (process.platform === 'darwin') {
return clipboard.readBuffer('public.file-url').toString();
} else if (process.platform === 'win32') {
return;
// reading files from clipboard for windows is not supported at the moment
//
// const rawFilePath = clipboard.read('FileNameW');
// return rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
}
};
Example #2
Source File: clipboard-saver.ts From awakened-poe-trade with MIT License | 6 votes |
export function restoreClipboard (cb: (clipboard: Clipboard) => void) {
// Not only do we not overwrite the clipboard,
// but we don't exec callback.
// This throttling helps against disconnects
// from "Too many actions".
if (!isRestored) {
return
}
isRestored = false
const saved = clipboard.readText()
cb(clipboard)
setTimeout(() => {
if (config.get('restoreClipboard')) {
clipboard.writeText(saved)
}
isRestored = true
}, THROTTLE)
}
Example #3
Source File: contextMenuBuilder.ts From TidGi-Desktop with Mozilla Public License 2.0 | 6 votes |
/**
* Adds "Copy Image URL" items when `src` is valid.
*/
private addImageItems(menu: Menu, menuInfo: IOnContextMenuInfo): Menu {
if (menuInfo.srcURL === undefined) return menu;
const copyImageUrl = new MenuItem({
label: this.stringTable.copyImageUrl(),
click: () => clipboard.writeText(menuInfo.srcURL!),
});
menu.append(copyImageUrl);
return menu;
}
Example #4
Source File: contextMenuBuilder.ts From TidGi-Desktop with Mozilla Public License 2.0 | 6 votes |
/**
* Builds a menu applicable to a link element.
*
* @return {Menu} The `Menu`
*/
private buildMenuForLink(menuInfo: IOnContextMenuInfo): Menu {
const menu = new Menu();
if (menuInfo.linkURL === undefined || menuInfo.linkText === undefined) return menu;
const isEmailAddress = menuInfo.linkURL.startsWith('mailto:');
const copyLink = new MenuItem({
label: isEmailAddress ? this.stringTable.copyMail() : this.stringTable.copyLinkUrl(),
click: () => {
// Omit the mailto: portion of the link; we just want the address
clipboard.writeText(isEmailAddress ? menuInfo.linkText! : menuInfo.linkURL!);
},
});
const openLink = new MenuItem({
label: this.stringTable.openLinkUrl(),
click: () => {
void shell.openExternal(menuInfo.linkURL!);
},
});
menu.append(copyLink);
menu.append(openLink);
if (this.isSrcUrlValid(menuInfo)) {
this.addSeparator(menu);
this.addImageItems(menu, menuInfo);
}
this.addInspectElement(menu, menuInfo);
this.addDeveloperTools(menu, menuInfo);
this.processMenu(menu, menuInfo);
return menu;
}
Example #5
Source File: preload.ts From noteworthy with GNU Affero General Public License v3.0 | 6 votes |
clipboardApi: WindowAfterPreload["clipboardApi"] = {
getClipboardImageDataURI(): string|null {
if(clipboard.availableFormats("clipboard").find(str => str.startsWith("image"))){
return clipboard.readImage("clipboard").toDataURL();
} else {
return null;
}
}
}
Example #6
Source File: clipboard-saver.ts From awakened-poe-trade with MIT License | 6 votes |
export function restoreClipboard (cb: (clipboard: Clipboard) => void) {
// Not only do we not overwrite the clipboard,
// but we don't exec callback.
// This throttling helps against disconnects
// from "Too many actions".
if (!isRestored) {
return
}
isRestored = false
const saved = clipboard.readText()
cb(clipboard)
setTimeout(() => {
if (config.get('restoreClipboard')) {
clipboard.writeText(saved)
}
isRestored = true
}, THROTTLE)
}
Example #7
Source File: process.ts From DittoPlusPlus with MIT License | 6 votes |
writeClipboard = (clipItem: ClipItemDoc) => {
switch (clipItem.type) {
case 'file':
this.lastClipId = clipItem.path;
this.writeClipboardFiles(clipItem);
break;
case 'text':
this.lastClipId = clipItem.text;
clipboard.writeText(clipItem.text);
break;
case 'image':
const image = nativeImage.createFromPath(
path.join(this.imagesDir, `${clipItem._id}.png`),
);
this.lastClipId = image.getBitmap().toString();
clipboard.writeImage(image);
break;
}
};
Example #8
Source File: process.ts From DittoPlusPlus with MIT License | 6 votes |
writeClipboardFiles = (clipItem: ClipItemDocFile) => {
if (process.platform === 'darwin') {
clipboard.writeBuffer(
'public.file-url',
Buffer.from(clipItem.path, 'utf-8'),
);
} else if (process.platform === 'win32') {
// writing files to clipboard for windows is not supported at the moment
//
// clipboard.writeBuffer(
// 'FileNameW',
// Buffer.from(clipItem.path.split('').join(String.fromCharCode(0))),
// 'clipboard',
// );
}
};
Example #9
Source File: process.ts From DittoPlusPlus with MIT License | 6 votes |
readClipboard = (): ClipData | undefined => {
const clipText = clipboard.readText();
const clipImageBuffer = clipboard.readImage();
const clipFile = this.readClipboardFiles();
let clipData: ClipData | undefined = undefined;
if (clipFile) {
clipData = {
type: 'file',
data: clipFile,
};
} else if (clipImageBuffer.getBitmap().length !== 0) {
clipData = {
type: 'image',
data: clipImageBuffer,
};
} else if (clipText) {
clipData = {
type: 'text',
data: clipText,
};
}
return clipData;
};
Example #10
Source File: poll-clipboard.ts From awakened-poe-trade with MIT License | 5 votes |
export async function pollClipboard (): Promise<string> {
isPollingClipboard = true
elapsed = 0
if (clipboardPromise) {
return await clipboardPromise
}
let textBefore = clipboard.readText()
if (isActiveLangItem(textBefore)) {
textBefore = ''
clipboard.writeText('')
}
clipboardPromise = new Promise((resolve, reject) => {
function poll () {
const textAfter = clipboard.readText()
if (isActiveLangItem(textAfter)) {
if (config.get('restoreClipboard')) {
clipboard.writeText(textBefore)
}
isPollingClipboard = false
clipboardPromise = undefined
resolve(textAfter)
} else {
elapsed += DELAY
if (elapsed < LIMIT) {
setTimeout(poll, DELAY)
} else {
if (config.get('restoreClipboard')) {
clipboard.writeText(textBefore)
}
isPollingClipboard = false
clipboardPromise = undefined
const otherLang = isInactiveLangItem(textAfter)
if (otherLang) {
logger.warn('Detected item in inactive or unsupported language.', { source: 'clipboard', language: otherLang.lang })
} else {
logger.warn('No item text found.', { source: 'clipboard', text: textAfter.slice(0, 40) })
}
reject(new Error('Reading clipboard timed out'))
}
}
}
poll()
})
return clipboardPromise
}
Example #11
Source File: base.ts From multi-downloader-nx with MIT License | 5 votes |
async writeToClipboard(text: string) {
clipboard.writeText(text, 'clipboard');
return true;
}
Example #12
Source File: uploaderManager.ts From Aragorn with MIT License | 5 votes |
async uploadFromClipboard() {
// https://github.com/njzydark/electron-clipboard-demo
console.log('upload from clipboard');
let filePath: (string | FileFormData)[] = [];
const clipboardImage = clipboard.readImage('clipboard');
if (!clipboardImage.isEmpty()) {
console.log('upload image from clipboard');
const png = clipboardImage.toPNG();
const fileInfo: FileFormData = {
buffer: png,
mimetype: 'image/png',
originalname: uuidv4() + '.png'
};
filePath = [fileInfo];
}
if (process.platform === 'darwin') {
// https://github.com/electron/electron/issues/9035#issuecomment-359554116
if (clipboard.has('NSFilenamesPboardType')) {
filePath =
clipboard
.read('NSFilenamesPboardType')
.match(/<string>.*<\/string>/g)
?.map(item => item.replace(/<string>|<\/string>/g, '')) || [];
} else {
if (filePath.length === 0) {
filePath = [clipboard.read('public.file-url').replace('file://', '')].filter(item => item);
}
}
} else {
// https://github.com/electron/electron/issues/9035#issuecomment-536135202
// https://docs.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop
// https://www.codeproject.com/Reference/1091137/Windows-Clipboard-Formats
if (clipboard.has('CF_HDROP')) {
const rawFilePathStr = clipboard.readBuffer('CF_HDROP').toString('ucs2');
let formatFilePathStr = [...rawFilePathStr]
.filter((_, index) => rawFilePathStr.charCodeAt(index) !== 0)
.join('')
.replace(/\\/g, '\\');
const drivePrefix = formatFilePathStr.match(/[a-zA-Z]:\\/);
if (drivePrefix) {
const drivePrefixIndex = formatFilePathStr.indexOf(drivePrefix[0]);
if (drivePrefixIndex !== 0) {
formatFilePathStr = formatFilePathStr.substring(drivePrefixIndex);
}
filePath = formatFilePathStr
.split(drivePrefix[0])
.filter(item => item)
.map(item => drivePrefix + item);
}
} else {
if (filePath.length === 0) {
filePath = [
clipboard
.readBuffer('FileNameW')
.toString('ucs2')
.replace(RegExp(String.fromCharCode(0), 'g'), '')
].filter(item => item);
}
}
}
console.log(`get file path from clipboard: ${filePath}`);
if (filePath.length > 0) {
this.upload({ files: filePath });
}
}
Example #13
Source File: uploaderManager.ts From Aragorn with MIT License | 5 votes |
protected handleSingleUploaded(successFile: UploadedFileInfo, failFile: UploadedFileInfo) {
if (successFile) {
// 根据urlType转换图片链接格式
let clipboardUrl = successFile.url;
switch (this.setting.configuration.urlType) {
case 'URL':
break;
case 'HTML':
clipboardUrl = `<img src="${successFile.url}" />`;
break;
case 'Markdown':
clipboardUrl = `![${successFile.url}](${successFile.url})`;
break;
default:
return successFile.url;
}
if (this.setting.configuration.autoCopy) {
let preClipBoardText = '';
if (this.setting.configuration.autoRecover) {
preClipBoardText = clipboard.readText();
}
// 开启自动复制
clipboard.writeText(clipboardUrl || '');
const notification = new Notification({
title: '上传成功',
body: '链接已自动复制到粘贴板',
silent: !this.setting.configuration.sound
});
this.setting.configuration.showNotifaction && notification.show();
this.setting.configuration.autoRecover &&
setTimeout(() => {
clipboard.writeText(preClipBoardText);
const notification = new Notification({
title: '粘贴板已恢复',
body: '已自动恢复上次粘贴板中的内容',
silent: !this.setting.configuration.sound
});
notification.show();
}, 5000);
} else {
const notification = new Notification({
title: '上传成功',
body: clipboardUrl || '',
silent: !this.setting.configuration.sound
});
this.setting.configuration.showNotifaction && notification.show();
}
} else {
const notification = new Notification({ title: '上传失败', body: failFile.errorMessage || '错误未捕获' });
notification.show();
}
}
Example #14
Source File: utils.ts From fluent-reader with BSD 3-Clause "New" or "Revised" License | 5 votes |
export function setUtilsListeners(manager: WindowManager) {
async function openExternal(url: string, background = false) {
if (url.startsWith("https://") || url.startsWith("http://")) {
if (background && process.platform === "darwin") {
shell.openExternal(url, { activate: false })
} else if (background && manager.hasWindow()) {
manager.mainWindow.setAlwaysOnTop(true)
await shell.openExternal(url)
setTimeout(() => manager.mainWindow.setAlwaysOnTop(false), 1000)
} else {
shell.openExternal(url)
}
}
}
app.on("web-contents-created", (_, contents) => {
contents.setWindowOpenHandler(details => {
if (contents.getType() === "webview")
openExternal(
details.url,
details.disposition === "background-tab"
)
return {
action: manager.hasWindow() ? "deny" : "allow",
}
})
contents.on("will-navigate", (event, url) => {
event.preventDefault()
if (contents.getType() === "webview") openExternal(url)
})
})
ipcMain.on("get-version", event => {
event.returnValue = app.getVersion()
})
ipcMain.handle("open-external", (_, url: string, background: boolean) => {
openExternal(url, background)
})
ipcMain.handle("show-error-box", (_, title, content) => {
dialog.showErrorBox(title, content)
})
ipcMain.handle(
"show-message-box",
async (_, title, message, confirm, cancel, defaultCancel, type) => {
if (manager.hasWindow()) {
let response = await dialog.showMessageBox(manager.mainWindow, {
type: type,
title: title,
message: message,
buttons:
process.platform === "win32"
? ["Yes", "No"]
: [confirm, cancel],
cancelId: 1,
defaultId: defaultCancel ? 1 : 0,
})
return response.response === 0
} else {
return false
}
}
)
ipcMain.handle(
"show-save-dialog",
async (_, filters: Electron.FileFilter[], path: string) => {
ipcMain.removeAllListeners("write-save-result")
if (manager.hasWindow()) {
let response = await dialog.showSaveDialog(manager.mainWindow, {
defaultPath: path,
filters: filters,
})
if (!response.canceled) {
ipcMain.handleOnce(
"write-save-result",
(_, result, errmsg) => {
fs.writeFile(response.filePath, result, err => {
if (err)
dialog.showErrorBox(errmsg, String(err))
})
}
)
return true
}
}
return false
}
)
ipcMain.handle(
"show-open-dialog",
async (_, filters: Electron.FileFilter[]) => {
if (manager.hasWindow()) {
let response = await dialog.showOpenDialog(manager.mainWindow, {
filters: filters,
properties: ["openFile"],
})
if (!response.canceled) {
try {
return await fs.promises.readFile(
response.filePaths[0],
"utf-8"
)
} catch (err) {
console.log(err)
}
}
}
return null
}
)
ipcMain.handle("get-cache", async () => {
return await session.defaultSession.getCacheSize()
})
ipcMain.handle("clear-cache", async () => {
await session.defaultSession.clearCache()
})
app.on("web-contents-created", (_, contents) => {
if (contents.getType() === "webview") {
contents.on(
"did-fail-load",
(event, code, desc, validated, isMainFrame) => {
if (isMainFrame && manager.hasWindow()) {
manager.mainWindow.webContents.send(
"webview-error",
desc
)
}
}
)
contents.on("context-menu", (_, params) => {
if (
(params.hasImageContents ||
params.selectionText ||
params.linkURL) &&
manager.hasWindow()
) {
if (params.hasImageContents) {
ipcMain.removeHandler("image-callback")
ipcMain.handleOnce(
"image-callback",
(_, type: ImageCallbackTypes) => {
switch (type) {
case ImageCallbackTypes.OpenExternal:
case ImageCallbackTypes.OpenExternalBg:
openExternal(
params.srcURL,
type ===
ImageCallbackTypes.OpenExternalBg
)
break
case ImageCallbackTypes.SaveAs:
contents.session.downloadURL(
params.srcURL
)
break
case ImageCallbackTypes.Copy:
contents.copyImageAt(params.x, params.y)
break
case ImageCallbackTypes.CopyLink:
clipboard.writeText(params.srcURL)
break
}
}
)
manager.mainWindow.webContents.send(
"webview-context-menu",
[params.x, params.y]
)
} else {
manager.mainWindow.webContents.send(
"webview-context-menu",
[params.x, params.y],
params.selectionText,
params.linkURL
)
}
contents
.executeJavaScript(
`new Promise(resolve => {
const dismiss = () => {
document.removeEventListener("mousedown", dismiss)
document.removeEventListener("scroll", dismiss)
resolve()
}
document.addEventListener("mousedown", dismiss)
document.addEventListener("scroll", dismiss)
})`
)
.then(() => {
if (manager.hasWindow()) {
manager.mainWindow.webContents.send(
"webview-context-menu"
)
}
})
}
})
contents.on("before-input-event", (_, input) => {
if (manager.hasWindow()) {
let contents = manager.mainWindow.webContents
contents.send("webview-keydown", input)
}
})
}
})
ipcMain.handle("write-clipboard", (_, text) => {
clipboard.writeText(text)
})
ipcMain.handle("close-window", () => {
if (manager.hasWindow()) manager.mainWindow.close()
})
ipcMain.handle("minimize-window", () => {
if (manager.hasWindow()) manager.mainWindow.minimize()
})
ipcMain.handle("maximize-window", () => {
manager.zoom()
})
ipcMain.on("is-maximized", event => {
event.returnValue =
Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
})
ipcMain.on("is-focused", event => {
event.returnValue =
manager.hasWindow() && manager.mainWindow.isFocused()
})
ipcMain.on("is-fullscreen", event => {
event.returnValue =
manager.hasWindow() && manager.mainWindow.isFullScreen()
})
ipcMain.handle("request-focus", () => {
if (manager.hasWindow()) {
const win = manager.mainWindow
if (win.isMinimized()) win.restore()
if (process.platform === "win32") {
win.setAlwaysOnTop(true)
win.setAlwaysOnTop(false)
}
win.focus()
}
})
ipcMain.handle("request-attention", () => {
if (manager.hasWindow() && !manager.mainWindow.isFocused()) {
if (process.platform === "win32") {
manager.mainWindow.flashFrame(true)
manager.mainWindow.once("focus", () => {
manager.mainWindow.flashFrame(false)
})
} else if (process.platform === "darwin") {
app.dock.bounce()
}
}
})
ipcMain.handle("touchbar-init", (_, texts: TouchBarTexts) => {
if (manager.hasWindow()) initMainTouchBar(texts, manager.mainWindow)
})
ipcMain.handle("touchbar-destroy", () => {
if (manager.hasWindow()) manager.mainWindow.setTouchBar(null)
})
ipcMain.handle("init-font-list", () => {
return fontList.getFonts({
disableQuoting: true,
})
})
}
Example #15
Source File: server.ts From shadowsocks-electron with GNU General Public License v3.0 | 5 votes |
async parseClipboardText(params: { text: string, type: ClipboardParseType }): Promise<ServiceResult> {
const text = params.text || clipboard.readText('clipboard');
const type = params.type || 'url';
if (type === 'url') return Promise.resolve({
code: 200,
result: parseUrl(text)
});
if (type === 'subscription') {
return parseSubscription(text).then(res => {
if (res.error) {
return {
code: 200,
result: {
name: '',
result: [],
url: text
}
};
}
return {
code: 200,
result: {
name: res.name || '',
result: res.result || [],
url: text
}
};
});
}
return Promise.resolve({
code: 200,
result: {
name: '',
result: []
}
});
}
Example #16
Source File: ConfShareDialog.tsx From shadowsocks-electron with GNU General Public License v3.0 | 5 votes |
MediaCard: React.FC<MediaCard> = (props) => {
const classes = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const enqueueSnackbar = (message: SnackbarMessage, options: Notification) => {
dispatch(enqueueSnackbarAction(message, options))
};
const downloadPicture = (dataLink: string) => {
setTimeout(() => {
props.onClose('share');
}, 1e3);
saveDataURLAsFile(dataLink, 'share');
}
const copyLink = (link: string) => {
setTimeout(() => {
props.onClose('share');
}, 1e3);
enqueueSnackbar(t('copied'), { variant: 'success' });
clipboard.writeText(link, 'clipboard');
}
return (
<StyledCard>
<CardActionArea>
<CardMedia
component="img"
className={classes.media}
image={props.dataUrl}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="h2"></Typography>
<Typography variant="body2" color="textSecondary" component="span">
<p className={classes.textOverflow}>
{props.url}
</p>
</Typography>
</CardContent>
</CardActionArea>
<Divider />
<CardActions className={classes.action}>
<Button
className={classes.button}
size="small" onClick={() => copyLink(props.url)}
endIcon={<FileCopyIcon />}
>
{t('copy_link')}
</Button>
<Button
className={classes.button}
size="small" onClick={() => downloadPicture(props.dataUrl)}
endIcon={<GetAppIcon />}
>
{t('save')}
</Button>
</CardActions>
</StyledCard>
);
}
Example #17
Source File: PostComment.tsx From 3Speak-app with GNU General Public License v3.0 | 4 votes |
/**
* @todo Implement displaying numbers of comments and comment value. Requires support on backend to count votes.
* @todo Implement interactibility.
* @todo Implement 3 dot action menu.
*/
export function PostComment(props: any) {
const [commentInfo, setCommentInfo] = useState({ description: '', creation: 0 })
const [replying, setReplying] = useState(false)
const [profilePicture, setProfilePicture] = useState('')
useEffect(() => {
const load = async () => {
let info
let profilePicture
if (props.commentInfo) {
info = props.commentInfo
} else {
info = await AccountService.permalinkToVideoInfo(props.reflink)
}
if (info) {
profilePicture = setProfilePicture(await AccountService.getProfilePictureURL(info.reflink))
setCommentInfo(info)
}
}
void load()
}, [])
const handleAction = async (eventKey) => {
const reflink = props.reflink
switch (eventKey) {
case 'block_post': {
await electronIpc.send('blocklist.add', reflink, {
reason: 'manual block',
})
break
}
case 'block_user': {
const ref = RefLink.parse(reflink) as any as any
await electronIpc.send('blocklist.add', `${ref.source.value}:${ref.root}`, {
reason: 'manual block',
})
break
}
case 'copy_reflink': {
clipboard.writeText(reflink, clipboard as any)
break
}
default: {
throw new Error(`Unrecognized action: ${eventKey}!`)
}
}
}
const postTimeDistance = useMemo(() => {
return millisecondsAsString((new Date() as any) - (new Date(commentInfo.creation) as any))
}, [commentInfo])
const postTime = useMemo(() => {
return DateAndTime.format(new Date(commentInfo.creation), 'YYYY/MM/DD HH:mm:ss')
}, [commentInfo])
return (
<div>
<div className="col">
<div className="thumbnail mr-2 float-left">
<img className="img-responsive user-photo" width="24" src={profilePicture} />
</div>
</div>
<div className="col" style={{ zIndex: 1000 }}>
<div className="mr-3 float-right">
<Dropdown onSelect={handleAction}>
<Dropdown.Toggle as={CustomToggle}></Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item style={{ color: 'red' }} eventKey="block_post">
Block post
</Dropdown.Item>
<Dropdown.Item style={{ color: 'red' }} eventKey="block_user">
Block user
</Dropdown.Item>
<Dropdown.Item eventKey="copy_reflink">Copy to clipboard permlink</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
</div>
<div className="col-12">
<div className="panel ml-2 panel-default">
<div className="panel-heading ml-4">
<strong>
<a href={`#/user?=${props.reflink}`}>{RefLink.parse(props.reflink).root}</a>{' '}
</strong>
•{' '}
<span className="text-muted">
<OverlayTrigger overlay={<Tooltip id="post-time">{postTime}</Tooltip>}>
<div>{postTimeDistance}</div>
</OverlayTrigger>
</span>
</div>
<div className="panel-body mt-1">
<ReactMarkdown
escapeHtml={false}
source={DOMPurify.sanitize(commentInfo.description)}
></ReactMarkdown>
</div>
<div className="panel-footer ml-0 ml-md-4">
<hr />
<ul className="list-inline list-inline-separate">
<li className="list-inline-item">
<VoteWidget reflink={props.reflink} />
</li>
<li
className="list-inline-item"
style={{ cursor: 'pointer' }}
onClick={() => {
setReplying(!replying)
}}
>
{replying ? 'Cancel' : 'Reply'}
</li>
</ul>
</div>
</div>
</div>
{replying ? (
<div className="box mb-3 clearfix">
<CommentForm parent_reflink={props.reflink} onCommentPost={props.onCommentPost} />
</div>
) : null}
</div>
)
}
Example #18
Source File: index.ts From TidGi-Desktop with Mozilla Public License 2.0 | 4 votes |
private async registerMenu(): Promise<void> {
await this.menuService.insertMenu('View', [
{ role: 'reload' },
{ role: 'forceReload' },
// `role: 'zoom'` is only supported on macOS
process.platform === 'darwin'
? {
role: 'zoom',
}
: {
label: 'Zoom',
click: () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.maximize();
}
},
},
{ role: 'resetZoom' },
{ role: 'togglefullscreen' },
{ role: 'close' },
]);
await this.menuService.insertMenu(
'View',
[
{
label: () => i18n.t('Menu.Find'),
accelerator: 'CmdOrCtrl+F',
click: async () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send(WindowChannel.openFindInPage);
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(await getViewBounds(contentSize as [number, number], true));
}
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindNext'),
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindPrevious'),
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => `${i18n.t('Preference.AlwaysOnTop')} (${i18n.t('Preference.RequireRestart')})`,
checked: async () => await this.preferenceService.get('alwaysOnTop'),
click: async () => {
const alwaysOnTop = await this.preferenceService.get('alwaysOnTop');
await this.preferenceService.set('alwaysOnTop', !alwaysOnTop);
await this.requestRestart();
},
},
],
// eslint-disable-next-line unicorn/no-null
null,
true,
);
await this.menuService.insertMenu('History', [
{
label: () => i18n.t('Menu.Home'),
accelerator: 'Shift+CmdOrCtrl+H',
click: async () => await this.goHome(),
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Back'),
accelerator: 'CmdOrCtrl+[',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
// TODO: test if we really can get this isPopup value
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
browserWindow.webContents.goBack();
return;
}
}
ipcMain.emit('request-go-back');
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Forward'),
accelerator: 'CmdOrCtrl+]',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
browserWindow.webContents.goBack();
return;
}
}
ipcMain.emit('request-go-forward');
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{ type: 'separator' },
{
label: () => i18n.t('ContextMenu.CopyLink'),
accelerator: 'CmdOrCtrl+L',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// copy the popup window URL instead
if (browserWindow !== undefined) {
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const url = browserWindow.webContents.getURL();
clipboard.writeText(url);
return;
}
}
const mainWindow = this.get(WindowNames.main);
const url = mainWindow?.getBrowserView()?.webContents?.getURL();
if (typeof url === 'string') {
clipboard.writeText(url);
}
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
]);
}
Example #19
Source File: index.tsx From electron-playground with MIT License | 4 votes |
CodeRunner: React.FunctionComponent<ICodeRunnerProps> = props => {
// const id = useMemo(() => randomId(), [])
const id = useRef(randomId())
const { code, src, height = '500px', mProcess = true } = props
const [value, setValue] = React.useState<string>(code || '')
const [stdout, setStdout] = useState<string>('')
const [codePath, setCodePath] = React.useState<string>('')
React.useEffect(() => {
if (!src) {
return
}
const codePath = getSrcRelativePath(src)
setValue('loading ......')
setCodePath(codePath)
fs.readFile(codePath, (err, content) => {
setValue(content.toString())
})
}, [src])
const onChange = (e: string) => {
setValue(e)
}
const executeMasterProcess = () => {
try {
const result = window.$EB.actionCode(value, id.current)
const out = result.reduce((prev: LogItem, curr: LogItem) => `${prev}${curr.content}\n`, '')
setStdout(out)
// 处理console
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.$EB.ipcRenderer.on('ACTION_CODE_LOG', (...args: any[]) => {
const res = args[1]
if (res[0].id === id.current) {
const out = res.reduce((prev: LogItem, curr: LogItem) => `${prev}${curr.content}\n`, '')
setStdout(out)
}
})
} catch (error) {
console.error('执行错误', error)
}
}
const executeRenderProcess = () => {
// 拦截log
const print = (...args: unknown[]) => {
if (!args.length) {
return
}
const content = args.reduce(
(prev, curr) => prev + util.inspect(curr, { showHidden: true }),
'',
) as string
setStdout(content)
}
const MockConsole = {
log: print,
error: print,
warn: print,
info: print,
debug: print,
}
const fn = new Function(
'require',
'console',
`return function(){
try{
${value}
}catch(error){
console.error('程序执行错误',error)
}
}`,
)(nativeRequire, MockConsole)
const result = fn()
if (result) {
MockConsole.log(result)
}
}
const copy = (text: string) => {
console.log('???', text)
clipboard.writeText(text)
message.info('已复制到剪贴板')
}
return (
<div className={style.container}>
{codePath && <p>代码路径: {codePath}</p>}
<main>
<div className={style.editor}>
<MonacoEditor code={code || ''} language='javascript' onChange={onChange} />
<EditorToolbar
tools={[
{ type: 'run', onClick: mProcess ? executeMasterProcess : executeRenderProcess },
{ type: 'copy', onClick: () => copy(value) },
]}
/>
</div>
{stdout && (
<div className={style.output}>
<MonacoEditor
key={`console-${stdout}`}
code={stdout}
language='javascript'
options={{ readOnly: true }}
/>
</div>
)}
</main>
</div>
)
}
Example #20
Source File: instance-api.ts From uivonim with GNU Affero General Public License v3.0 | 4 votes |
InstanceApi = (workerInstanceRef: Worker, winRef: BrowserWindow) => {
const ee = new EventEmitter()
const { state, watchState, onStateValue, onStateChange, untilStateValue } =
NeovimState('nvim-mirror')
const actionRegistrations: string[] = []
if (actionRegistrations.length)
actionRegistrations.forEach((name) => workerInstanceRef.call.onAction(name))
workerInstanceRef.on.nvimStateUpdate((stateDiff: any) => {
// TODO: do we need this to always be updated or can we query these values?
// this will trigger on every cursor move and take up time in the render cycle
Object.assign(state, stateDiff)
})
// TODO(smolck): Used to return promise here, probably fine now but just a
// note.
workerInstanceRef.on.showNeovimMessage((...a: any[]) =>
winRef.webContents.send(Events.nvimShowMessage, a)
)
workerInstanceRef.on.showStatusBarMessage((message: string) => {
winRef.webContents.send(Events.nvimMessageStatus, message)
})
workerInstanceRef.on.vimrcLoaded(() => ee.emit('nvim.load', false))
workerInstanceRef.on.gitStatus((status: GitStatus) =>
ee.emit('git.status', status)
)
workerInstanceRef.on.gitBranch((branch: string) =>
ee.emit('git.branch', branch)
)
workerInstanceRef.on.actionCalled((name: string, args: any[]) =>
ee.emit(`action.${name}`, ...args)
)
workerInstanceRef.on.clipboardRead(async () => clipboard.readText())
workerInstanceRef.on.clipboardWrite((text: string) =>
clipboard.writeText(text)
)
return {
state,
watchState,
onStateValue,
onStateChange,
untilStateValue,
getBufferInfo: (): Promise<BufferInfo> =>
workerInstanceRef.request.getBufferInfo(),
setMode: (mode: VimMode) => {
Object.assign(state, { mode })
workerInstanceRef.call.setNvimMode(mode)
},
getWindowMetadata: (): Promise<WindowMetadata[]> => {
return workerInstanceRef.request.getWindowMetadata()
},
onAction: (name: string, fn: (...args: any[]) => void) => {
if (typeof fn !== 'function')
throw new Error(`nvim.onAction needs a function for event ${name}`)
actionRegistrations.push(name)
ee.on(`action.${name}`, fn)
try {
workerInstanceRef.call.onAction(name)
} catch (_) {
// not worried if no instance, we will register later in 'onCreateVim'
}
},
gitOnStatus: (fn: (status: GitStatus) => void) => ee.on('git.status', fn),
gitOnBranch: (fn: (branch: string) => void) => ee.on('git.branch', fn),
bufferSearch: (file: string, query: string) =>
workerInstanceRef.request.bufferSearch(file, query),
bufferSearchVisible: (query: string) =>
workerInstanceRef.request.bufferSearchVisible(query),
onNvimLoaded: (fn: (switchInstance: boolean) => void) =>
ee.on('nvim.load', fn),
nvimGetVar: (key: string) => workerInstanceRef.request.nvimGetVar(key),
nvimCommand: (command: string) =>
workerInstanceRef.call.nvimCommand(command),
nvimFeedkeys: (keys: string, mode = 'm') =>
workerInstanceRef.call.nvimFeedkeys(keys, mode),
nvimExpr: (expr: string) => workerInstanceRef.request.nvimExpr(expr),
nvimCall: onFnCall((name, a) =>
workerInstanceRef.request.nvimCall(name, a)
) as Functions,
nvimJumpTo: (coords: HyperspaceCoordinates) =>
workerInstanceRef.call.nvimJumpTo(coords),
nvimGetKeymap: () => workerInstanceRef.request.nvimGetKeymap(),
nvimGetColorByName: (name: string) =>
workerInstanceRef.request.nvimGetColorByName(name),
nvimSaveCursor: async () => {
const instance = workerInstanceRef
const pos = await workerInstanceRef.request.nvimSaveCursor()
return () => instance.call.nvimRestoreCursor(pos)
},
getHighlightByName: (
name: string,
isRgb?: boolean
): Promise<{
foreground?: number
background?: number
reverse?: boolean
}> => workerInstanceRef.request.nvimGetHighlightByName(name, isRgb),
nvimHighlightSearchPattern: (
pattern: string,
id?: number
): Promise<number> =>
workerInstanceRef.request.nvimHighlightSearchPattern(pattern, id),
nvimRemoveHighlightSearch: (
id: number,
pattern?: string
): Promise<boolean> =>
workerInstanceRef.request.nvimRemoveHighlightSearch(id, pattern),
onInputRemapModifiersDidChange: (fn: (modifiers: any[]) => void) =>
ee.on('input.remap.modifiers', fn),
onInputKeyTransformsDidChange: (fn: (transforms: any[]) => void) =>
ee.on('input.key.transforms', fn),
nvimOption: (opt: string): Promise<any> =>
workerInstanceRef.request.nvimOption(opt),
}
}
Example #21
Source File: handlers.ts From sapio-studio with Mozilla Public License 2.0 | 4 votes |
export default function (window: BrowserWindow) {
setup_chat();
ipcMain.handle('bitcoin::command', async (event, arg) => {
const node = await get_bitcoin_node();
try {
return { ok: await node.command(arg) };
} catch (r: any) {
if (r instanceof RpcError) {
return {
err: { code: r.code, message: r.message, name: r.name },
};
} else if (r instanceof Error) {
return { err: r.toString() };
}
}
});
ipcMain.handle('emulator::kill', (event) => {
kill_emulator();
});
ipcMain.handle('emulator::start', (event) => {
start_sapio_oracle();
});
ipcMain.handle('emulator::read_log', (event) => {
return get_emulator_log();
});
ipcMain.handle('sapio::load_contract_list', async (event, workspace) => {
const contracts = await sapio.list_contracts(workspace);
return contracts;
});
ipcMain.handle(
'sapio::create_contract',
async (event, workspace, [which, psbt, args]) => {
const result = await sapio.create_contract(
workspace,
which,
psbt,
args
);
return result;
}
);
ipcMain.handle('sapio::show_config', async (event) => {
return await sapio.show_config();
});
ipcMain.handle('sapio::load_wasm_plugin', (event, workspace) => {
const plugins = dialog.showOpenDialogSync({
properties: ['openFile', 'multiSelections'],
filters: [{ extensions: ['wasm'], name: 'WASM' }],
});
const errs = [];
if (!plugins || !plugins.length) return { err: 'No Plugin Selected' };
for (const plugin of plugins) {
const loaded = sapio.load_contract_file_name(workspace, plugin);
if ('err' in loaded) {
return loaded;
}
}
return { ok: null };
});
ipcMain.handle('sapio::open_contract_from_file', (event) => {
const file = dialog.showOpenDialogSync(window, {
properties: ['openFile'],
filters: [
{
extensions: ['json'],
name: 'Sapio Contract Object',
},
],
});
if (file && file.length === 1) {
const data = readFileSync(file[0]!, {
encoding: 'utf-8',
});
return { ok: data };
}
});
ipcMain.handle(
'sapio::compiled_contracts::list',
async (event, workspace) => {
return (
await SapioWorkspace.new(workspace)
).list_compiled_contracts();
}
);
ipcMain.handle(
'sapio::compiled_contracts::trash',
async (event, workspace, file_name) => {
return (
await SapioWorkspace.new(workspace)
).trash_compiled_contract(file_name);
}
);
ipcMain.handle('sapio::psbt::finalize', (event, psbt) => {
return sapio.psbt_finalize(psbt);
});
ipcMain.handle(
'sapio::compiled_contracts::open',
async (event, workspace_name, file_name) => {
const workspace = await SapioWorkspace.new(workspace_name);
const data = await workspace.read_bound_data_for(file_name);
const name = await workspace.read_module_for(file_name);
const args = await workspace.read_args_for(file_name);
return { ok: { data, name, args } };
}
);
ipcMain.handle('sapio::workspaces::init', async (event, workspace) => {
await SapioWorkspace.new(workspace);
});
ipcMain.handle('sapio::workspaces::list', async (event) => {
return await SapioWorkspace.list_all();
});
ipcMain.handle('sapio::workspaces::trash', async (event, workspace) => {
return (await SapioWorkspace.new(workspace)).trash_workspace(workspace);
});
ipcMain.handle('write_clipboard', (event, s: string) => {
clipboard.writeText(s);
});
ipcMain.handle('save_psbt', async (event, psbt) => {
const path = await dialog.showSaveDialog(window, {
filters: [
{
extensions: ['psbt'],
name: 'Partially Signed Bitcoin Transaction',
},
],
});
if (path.filePath) {
await writeFile(path.filePath, psbt);
}
});
ipcMain.handle('fetch_psbt', async (event, psbt) => {
const path = await dialog.showOpenDialog(window, {
filters: [
{
extensions: ['psbt'],
name: 'Partially Signed Bitcoin Transaction',
},
],
});
if (path && path.filePaths.length) {
return await readFile(path.filePaths[0]!, { encoding: 'utf-8' });
}
});
ipcMain.handle('save_contract', async (event, psbt) => {
const path = await dialog.showSaveDialog(window, {
filters: [{ extensions: ['json'], name: 'Sapio Contract Object' }],
});
if (path.filePath) {
await writeFile(path.filePath, psbt);
}
});
ipcMain.handle(
'save_settings',
async (event, which: Prefs, data: string) => {
return preferences.save(which, JSON.parse(data));
}
);
ipcMain.handle('load_settings_sync', (event, which: Prefs) => {
return preferences.data[which];
});
ipcMain.handle('select_filename', async (event) => {
const path = await dialog.showOpenDialog(window);
if (path && path.filePaths.length == 1) {
return path.filePaths[0]!;
}
return null;
});
}
Example #22
Source File: index.ts From electron-browser-shell with GNU General Public License v3.0 | 4 votes |
buildChromeContextMenu = (opts: ChromeContextMenuOptions): Menu => {
const { params, webContents, openLink, extensionMenuItems } = opts
const labels = opts.labels || opts.strings || LABELS
const menu = new Menu()
const append = (opts: Electron.MenuItemConstructorOptions) => menu.append(new MenuItem(opts))
const appendSeparator = () => menu.append(new MenuItem({ type: 'separator' }))
if (params.linkURL) {
append({
label: labels.openInNewTab('link'),
click: () => {
openLink(params.linkURL, 'default', params)
},
})
append({
label: labels.openInNewWindow('link'),
click: () => {
openLink(params.linkURL, 'new-window', params)
},
})
appendSeparator()
append({
label: labels.copyAddress('link'),
click: () => {
clipboard.writeText(params.linkURL)
},
})
appendSeparator()
} else if (params.mediaType !== 'none') {
// TODO: Loop, Show controls
append({
label: labels.openInNewTab(params.mediaType),
click: () => {
openLink(params.srcURL, 'default', params)
},
})
append({
label: labels.copyAddress(params.mediaType),
click: () => {
clipboard.writeText(params.srcURL)
},
})
appendSeparator()
}
if (params.isEditable) {
if (params.misspelledWord) {
for (const suggestion of params.dictionarySuggestions) {
append({
label: suggestion,
click: () => webContents.replaceMisspelling(suggestion),
})
}
if (params.dictionarySuggestions.length > 0) appendSeparator()
append({
label: labels.addToDictionary,
click: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
})
} else {
if (
app.isEmojiPanelSupported() &&
!['number', 'tel', 'other'].includes(params.inputFieldType)
) {
append({
label: labels.emoji,
click: () => app.showEmojiPanel(),
})
appendSeparator()
}
append({
label: labels.redo,
enabled: params.editFlags.canRedo,
click: () => webContents.redo(),
})
append({
label: labels.undo,
enabled: params.editFlags.canUndo,
click: () => webContents.undo(),
})
}
appendSeparator()
append({
label: labels.cut,
enabled: params.editFlags.canCut,
click: () => webContents.cut(),
})
append({
label: labels.copy,
enabled: params.editFlags.canCopy,
click: () => webContents.copy(),
})
append({
label: labels.paste,
enabled: params.editFlags.canPaste,
click: () => webContents.paste(),
})
append({
label: labels.delete,
enabled: params.editFlags.canDelete,
click: () => webContents.delete(),
})
appendSeparator()
if (params.editFlags.canSelectAll) {
append({
label: labels.selectAll,
click: () => webContents.selectAll(),
})
appendSeparator()
}
} else if (params.selectionText) {
append({
label: labels.copy,
click: () => {
clipboard.writeText(params.selectionText)
},
})
appendSeparator()
}
if (menu.items.length === 0) {
const browserWindow = getBrowserWindowFromWebContents(webContents)
// TODO: Electron needs a way to detect whether we're in HTML5 full screen.
// Also need to properly exit full screen in Blink rather than just exiting
// the Electron BrowserWindow.
if (browserWindow?.fullScreen) {
append({
label: labels.exitFullScreen,
click: () => browserWindow.setFullScreen(false),
})
appendSeparator()
}
append({
label: labels.back,
enabled: webContents.canGoBack(),
click: () => webContents.goBack(),
})
append({
label: labels.forward,
enabled: webContents.canGoForward(),
click: () => webContents.goForward(),
})
append({
label: labels.reload,
click: () => webContents.reload(),
})
appendSeparator()
}
if (extensionMenuItems) {
extensionMenuItems.forEach((item) => menu.append(item))
if (extensionMenuItems.length > 0) appendSeparator()
}
append({
label: labels.inspect,
click: () => {
webContents.inspectElement(params.x, params.y)
if (!webContents.isDevToolsFocused()) {
webContents.devToolsWebContents?.focus()
}
},
})
return menu
}
Example #23
Source File: ScanQRStep.tsx From deskreen with GNU Affero General Public License v3.0 | 4 votes |
ScanQRStep: React.FC = () => {
const { t } = useTranslation();
const classes = useStyles();
const { isDarkTheme } = useContext(SettingsContext);
const [roomID, setRoomID] = useState('');
const [LOCAL_LAN_IP, setLocalLanIP] = useState('');
useEffect(() => {
const getRoomIdInterval = setInterval(async () => {
const roomId = await ipcRenderer.invoke(
IpcEvents.GetWaitingForConnectionSharingSessionRoomId
);
if (roomId) {
setRoomID(roomId);
}
}, 1000);
const ipInterval = setInterval(async () => {
const gotIP = await ipcRenderer.invoke('get-local-lan-ip');
if (gotIP) {
setLocalLanIP(gotIP);
}
}, 1000);
return () => {
clearInterval(getRoomIdInterval);
clearInterval(ipInterval);
};
}, []);
const [isQRCodeMagnified, setIsQRCodeMagnified] = useState(false);
return (
<>
<div style={{ textAlign: 'center' }}>
<Text>
<span
style={{
backgroundColor: '#00f99273',
fontWeight: 900,
paddingRight: '8px',
paddingLeft: '8px',
borderRadius: '20px',
}}
>
{t(
'Make sure your computer and screen viewing device are connected to same Wi-Fi'
)}
</span>
</Text>
<Text className="bp3-text">{t('Scan the QR code')}</Text>
</div>
<div>
<Tooltip content={t('Click to make bigger')} position={Position.LEFT}>
<Button
id="magnify-qr-code-button"
className={classes.smallQRCode}
onClick={() => setIsQRCodeMagnified(true)}
>
<QRCode
value={`http://${LOCAL_LAN_IP}:${CLIENT_VIEWER_PORT}/${roomID}`}
level="H"
renderAs="svg"
bgColor="rgba(0,0,0,0.0)"
fgColor={isDarkTheme ? '#ffffff' : '#000000'}
imageSettings={{
// TODO: change image to app icon
src: `http://127.0.0.1:${CLIENT_VIEWER_PORT}/logo192.png`,
width: 40,
height: 40,
}}
/>
</Button>
</Tooltip>
</div>
<div style={{ marginBottom: '10px' }}>
<Text className="bp3-text-muted">
{`${t(
'Or type the following address in browser address bar on any device'
)}:`}
</Text>
</div>
<Tooltip content={t('Click to copy')} position={Position.LEFT}>
<Button
intent="primary"
icon="duplicate"
style={{ borderRadius: '100px' }}
onClick={() => {
clipboard.writeText(
`http://${LOCAL_LAN_IP}:${CLIENT_VIEWER_PORT}/${roomID}`
);
}}
>
{`http://${LOCAL_LAN_IP}:${CLIENT_VIEWER_PORT}/${roomID}`}
</Button>
</Tooltip>
<Dialog
// @ts-ignore
id="bp3-qr-code-dialog-root"
className={classes.bigQRCodeDialogRoot}
isOpen={isQRCodeMagnified}
onClose={() => setIsQRCodeMagnified(false)}
canEscapeKeyClose
canOutsideClickClose
transitionDuration={isProduction() ? 700 : 0}
style={{ position: 'relative', top: '0px' }}
usePortal={false}
>
<Row
id="qr-code-dialog-inner"
className={Classes.DIALOG_BODY}
center="xs"
middle="xs"
onClick={() => setIsQRCodeMagnified(false)}
>
<Col xs={11} className={classes.dialogQRWrapper}>
<QRCode
value={`http://${LOCAL_LAN_IP}:${CLIENT_VIEWER_PORT}/${roomID}`}
level="H"
renderAs="svg"
imageSettings={{
// TODO: change image to app icon
src: `http://127.0.0.1:${CLIENT_VIEWER_PORT}/logo192.png`,
width: 25,
height: 25,
}}
width="390px"
height="390px"
/>
</Col>
</Row>
</Dialog>
</>
);
}
Example #24
Source File: ServerListItemSingle.tsx From shadowsocks-electron with GNU General Public License v3.0 | 4 votes |
ServerListItemSingle: React.FC<ServerListItemSingleProps> = props => {
const styles = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const {
selected,
connected,
onEdit,
onShare,
onRemove,
item,
topable = true,
moveable = true,
deleteable = true,
handleServerConnect,
handleServerSelect,
} = props;
const {
id,
remark,
type,
serverHost,
serverPort,
plugin,
} = item;
const origin = `${serverHost}:${serverPort}`;
const [actionHidden, setActionHidden] = useState(true);
const [ContextMenu, handleMenuOpen] = useContextMenu(getMenuContents());
const secondaryText =
(remark && plugin) ?
`${origin} / ${plugin}` :
remark ?
origin :
plugin ?
plugin :
"";
function getMenuContents () {
return [
{
label: (connected && selected) ? t('disconnect') : t('connect'),
action: (connected && selected) ? ('disconnect') : ('connect'),
icon: (connected && selected) ? <WifiOffIcon fontSize="small" /> : <WifiIcon fontSize="small" />
},
{ label: t('copy'), action: 'copy', icon: <CopyIcon fontSize="small" /> },
{ label: t('delay_test'), action: 'test', icon: <SettingsEthernetIcon fontSize="small" />},
...topable ? [
{ label: t('top'), action: 'top', icon: <VerticalAlignTopIcon fontSize="small" />},
] : [],
...moveable ? [{
label: t('move_up'), action: 'move_up', icon: <ArrowUpwardIcon fontSize="small" />},
{ label: t('move_down'), action: 'move_down', icon: <ArrowDownwardIcon fontSize="small" /> }] : [],
];
}
const handleActionHide = () => {
setActionHidden(true);
};
const handleActionShow = () => {
setActionHidden(false);
};
const handleEditButtonClick = () => {
onEdit?.(id);
};
const handleShareButtonClick = () => {
onShare?.(id);
};
const handleRemoveButtonClick = () => {
onRemove?.(id);
};
const handleChooseButtonClick = () => {
if (selected) {
if (connected) {
handleServerConnect(id);
} else {
handleServerConnect(id);
}
} else {
if (connected) {
handleServerSelect(id);
} else {
handleServerSelect(id);
setTimeout(() => {
handleServerConnect(id);
}, 300);
}
}
}
const handleContextMenuClick = (action: string) => {
switch (action) {
case 'connect':
handleChooseButtonClick();
break;
case 'disconnect':
handleChooseButtonClick();
break;
case 'copy':
clipboard.writeText(JSON.stringify(item));
break;
case 'test':
dispatch(getConnectionDelay(serverHost, serverPort));
break;
case 'top':
dispatch(top(item.id));
break;
case 'move_up':
dispatch(moveUp(id));
break;
case 'move_down':
dispatch(moveDown(id));
break;
default:
break;
}
}
const onContextMenu = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
handleMenuOpen(e);
};
return (
<div
onMouseEnter={handleActionShow}
onMouseLeave={handleActionHide}
>
<ListItem button onClick={handleChooseButtonClick} onContextMenu={onContextMenu}>
<ListItemIcon
className={styles.listIcon}
>
{
selected ? (
<CheckBoxIcon className={styles.lighterPrimary} />
) :
<CheckBoxOutlineBlankIcon />
}
</ListItemIcon>
<ListItemTextMultibleLine
primary={
<StyledBadge
badgeContent={type} color="primary"
>
<span>{remark ? remark : origin}</span>
</StyledBadge>
}
secondary={
// <TextEllipsis text={secondaryText as string} />
secondaryText
}
/>
<ListItemSecondaryAction
className={styles.action}
hidden={actionHidden}
>
<Tooltip title={(t('share') as string)}>
<IconButton edge="end" onClick={handleShareButtonClick} size="small">
<ShareIcon />
</IconButton>
</Tooltip>
<Tooltip title={(t('edit') as string)}>
<IconButton edge="end" onClick={handleEditButtonClick} size="small">
<EditIcon />
</IconButton>
</Tooltip>
{
deleteable && (
<Tooltip title={(t('delete') as string)}>
<IconButton edge="end" onClick={handleRemoveButtonClick} size="small" className={styles.deleteButton}>
<RemoveIcon />
</IconButton>
</Tooltip>
)
}
</ListItemSecondaryAction>
</ListItem>
<ContextMenu onItemClick={handleContextMenuClick} />
</div>
);
}
Example #25
Source File: ServerListItemGroup.tsx From shadowsocks-electron with GNU General Public License v3.0 | 4 votes |
ServerListItemGroup: React.FC<ServerListItemGroupProps> = props => {
// const styles = useStyles();
const dispatch = useDispatch();
const enqueueSnackbar = (message: SnackbarMessage, options: Notification) => {
dispatch(enqueueSnackbarAction(message, options))
};
const { t } = useTranslation();
const {
item,
selectedServer
} = props;
const [expanded, handleChange] = useState(!!item.servers?.find(server => server.id === selectedServer));
const [ContextMenu, handleMenuOpen] = useContextMenu([
{ label: t('copy'), action: 'copy', icon: <CopyIcon fontSize="small" /> },
{ label: t('update'), action: 'update_subscription', icon: <Refresh fontSize="small" /> },
{ label: t('top'), action: 'top', icon: <VerticalAlignTopIcon fontSize="small" />},
{ label: t('move_up'), action: 'move_up', icon: <ArrowUpwardIcon fontSize="small" /> },
{ label: t('move_down'), action: 'move_down', icon: <ArrowDownwardIcon fontSize="small" /> },
{ label: t('delete'), action: 'delete', icon: <DeleteIcon fontSize="small" />}
]);
useEffect(() => {
handleChange(!!item.servers?.find(server => server.id === selectedServer));
}, [selectedServer]);
const handleRemoveButtonClick = () => {
props.onRemove?.(item.id);
};
function onContextMenuClick (action: string) {
switch (action) {
case 'copy':
clipboard.writeText(JSON.stringify(item));
break;
case 'update_subscription':
if (item.url) {
dispatch(updateSubscription(item.id, item.url, {
success: t('subscription_updated'),
error: t('failed_to_update_subscription')
}));
} else {
enqueueSnackbar(t('server_url_not_set'), { variant: 'warning' });
}
break;
case 'top':
dispatch(top(item.id));
break;
case 'move_up':
dispatch(moveUp(item.id));
break;
case 'move_down':
dispatch(moveDown(item.id));
break;
case 'delete':
handleRemoveButtonClick();
default:
break;
}
}
const onContextMenu = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
handleMenuOpen(e);
};
return (
<div
>
<Accordion expanded={expanded} onChange={() => handleChange(!expanded)}>
<StyledAccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel1bh-content"
onContextMenu={onContextMenu}
>
{ item.name }
</StyledAccordionSummary>
<StyledAccordionDetails>
{
item.servers.map(server => (
<ServerListItemSingle
selected={selectedServer === server.id}
moveable={false}
deleteable={false}
topable={false}
key={server.id}
{...props}
item={server}
/>
))
}
</StyledAccordionDetails>
</Accordion>
<ContextMenu onItemClick={onContextMenuClick} />
</div>
);
}
Example #26
Source File: index.tsx From Aragorn with MIT License | 4 votes |
FileManage = () => {
const {
state: {
uploaderProfiles,
configuration: { defaultUploaderProfileId }
}
} = useAppContext();
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
useEffect(() => {
function handleResize() {
setWindowHeight(window.innerHeight);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { id } = useParams<{ id: string }>();
const [hasFileManageFeature, setHasFileManageFeature] = useState(false);
const [uploaderProfile, setUploaderProfile] = useState({} as UploaderProfile);
useEffect(() => {
const currentId = id || defaultUploaderProfileId;
setCurrentProfile(currentId as string);
}, []);
useEffect(() => {
if (uploaderProfile?.id) {
getList();
}
}, [uploaderProfile]);
const [list, setList] = useState([] as ListFile[]);
const [listLoading, setListLoading] = useState(false);
const getList = (directoryPath?: string) => {
setListLoading(true);
ipcRenderer.send('file-list-get', uploaderProfile.id, directoryPath);
};
const [dirPath, setDirPath] = useState([] as string[]);
useEffect(() => {
function handleListGetReply(_, res?: FileListResponse) {
setListLoading(false);
if (res === undefined) {
setHasFileManageFeature(false);
setList([]);
message.info(`${uploaderProfile.uploaderName}暂不支持文件管理功能`);
return;
}
setHasFileManageFeature(true);
if (res.success) {
setList(res.data);
} else {
message.error(`文件列表获取失败 ${res.desc || ''}`);
}
}
function handleFileDeleteReply(_, res?: DeleteFileResponse) {
if (res === undefined) {
return;
}
if (res.success) {
message.success({ content: '文件删除成功', key: 'file-manage-delete' });
getList(dirPath.join('/'));
} else {
message.error({ content: `文件删除失败 ${res.desc || ''}`, key: 'file-manage-delete' });
}
}
function handleFileUploadReply() {
getList(dirPath.join('/'));
}
function handleDirectoryCreateReply(_, res?: CreateDirectoryResponse) {
if (res === undefined) {
return;
}
if (res.success) {
message.success('目录创建成功');
setModalVisible(false);
getList(dirPath.join('/'));
} else {
message.error(`目录创建失败 ${res.desc || ''}`);
}
}
function handleExportReplay(_, res) {
setExportLoading(false);
if (res) {
shell.showItemInFolder(res);
setRowKeys([]);
setSelectRows([]);
}
}
ipcRenderer.on('file-list-get-reply', handleListGetReply);
ipcRenderer.on('file-delete-reply', handleFileDeleteReply);
ipcRenderer.on('file-upload-reply', handleFileUploadReply);
ipcRenderer.on('directory-create-reply', handleDirectoryCreateReply);
ipcRenderer.on('export-reply', handleExportReplay);
return () => {
ipcRenderer.removeListener('file-list-get-reply', handleListGetReply);
ipcRenderer.removeListener('file-delete-reply', handleFileDeleteReply);
ipcRenderer.removeListener('file-upload-reply', handleFileUploadReply);
ipcRenderer.removeListener('directory-create-reply', handleDirectoryCreateReply);
ipcRenderer.removeListener('export-reply', handleExportReplay);
};
}, [uploaderProfile, dirPath]);
const handleNameClick = (record: ListFile) => {
if (record.type === 'directory') {
const newPath = [...dirPath, formatFileName(record.name)];
setDirPath(newPath);
getList(newPath.join('/'));
} else {
clipboard.writeText(record.url as string);
message.success('链接已复制到粘贴板');
}
};
const handlePathClick = (index: number) => {
if (index === -1) {
setDirPath([]);
getList();
} else {
const newPath = dirPath.slice(0, index + 1);
setDirPath(newPath);
getList(newPath.join('/'));
}
};
const setCurrentProfile = (uploaderProfileId: string) => {
setDirPath([]);
const uploaderProfile = uploaderProfiles.find(item => item.id === uploaderProfileId);
setUploaderProfile(uploaderProfile as UploaderProfile);
};
const formatFileName = (name: string) => {
if (dirPath.length > 0) {
const pathPrefix = dirPath.join('/') + '/';
return name.split(pathPrefix).pop() || '';
} else {
return name;
}
};
const [selectRowKeys, setRowKeys] = useState([] as string[]);
const [selectRows, setSelectRows] = useState([] as ListFile[]);
const handleTableRowChange = (selectedRowKeys, selectedRows: ListFile[]) => {
setRowKeys(selectedRowKeys);
setSelectRows(selectedRows);
};
const handleRefresh = () => {
getList(dirPath.join('/'));
};
const handleBatchDelete = () => {
Modal.confirm({
title: '确认删除',
onOk: () => {
const names = selectRows.map(item => [...dirPath, formatFileName(item.name)].join('/'));
message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
ipcRenderer.send('file-delete', uploaderProfile.id, names);
}
});
};
const handleDelete = (record: ListFile) => {
let name = record.name;
Modal.confirm({
title: '确认删除',
content: name,
onOk: () => {
let name = record.name;
if (record.type === 'directory') {
name = `${[...dirPath, record.name].join('/')}/`;
} else {
name = [...dirPath, formatFileName(record.name)].join('/');
}
message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
ipcRenderer.send('file-delete', uploaderProfile.id, [name]);
}
});
};
const uploadRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (event: React.FormEvent<HTMLInputElement>) => {
const fileList = event.currentTarget.files || [];
const filesPath = Array.from(fileList).map(file => file.path);
const pathPrefix = dirPath.join('/');
ipcRenderer.send('file-upload', uploaderProfile.id, filesPath, pathPrefix);
event.currentTarget.value = '';
};
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const handleCreateDirectory = () => {
form.validateFields().then(values => {
ipcRenderer.send('directory-create', uploaderProfile.id, values?.directoryPath || '');
});
};
const handleDownload = (record: ListFile) => {
ipcRenderer.send('file-download', record.name, record.url);
};
const [exportLoading, setExportLoading] = useState(false);
const handleExport = () => {
const data = selectRows.map(item => {
const fileNameArr = item.name.split('.');
fileNameArr.pop();
return {
name: fileNameArr.join('.'),
url: item.url
};
});
setExportLoading(true);
ipcRenderer.send('export', data);
};
const columns: ColumnsType<ListFile> = [
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
render: (val: string, record: ListFile) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
{record.type === 'directory' ? (
<FolderFilled style={{ fontSize: 16 }} />
) : (
<FileOutlined style={{ fontSize: 16 }} />
)}
{record.type === 'directory' ? (
<a
title={val}
onClick={() => handleNameClick(record)}
className="table-filename"
style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{formatFileName(val)}
</a>
) : (
<Popover
placement="topLeft"
content={() =>
/(jpg|png|gif|jpeg)$/.test(val) ? (
<Image
style={{ maxWidth: 500 }}
src={record.url}
fallback="data:image/png;base64,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"
/>
) : (
val
)
}
trigger="hover"
>
<a
title={val}
onClick={() => handleNameClick(record)}
className="table-filename"
style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{formatFileName(val)}
</a>
</Popover>
)}
</div>
)
},
{
title: '文件大小',
dataIndex: 'size',
ellipsis: true,
width: 120,
render: val => (val ? filesize(val) : '-')
},
{
title: '更新时间',
dataIndex: 'lastModified',
ellipsis: true,
width: 200,
render: val => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '-')
},
{
title: '操作',
width: 120,
render: (_, record) => (
<Space>
{record.type !== 'directory' && (
<>
<DownloadOutlined onClick={() => handleDownload(record)} />
<CopyOutlined onClick={() => handleNameClick(record)} />
</>
)}
<DeleteOutlined onClick={() => handleDelete(record)} />
</Space>
)
}
];
return (
<div className="storage-page">
<header>
<span>文件管理</span>
<Divider />
</header>
<Space style={{ marginBottom: 10 }}>
<Select style={{ minWidth: 120 }} value={uploaderProfile?.id} onChange={setCurrentProfile}>
{uploaderProfiles.map(item => (
<Select.Option key={item.name} value={item.id}>
{item.name}
</Select.Option>
))}
</Select>
<Button
title="上传"
icon={<UploadOutlined />}
disabled={!hasFileManageFeature}
type="primary"
onClick={() => {
uploadRef.current?.click();
}}
/>
<Button title="刷新" icon={<ReloadOutlined />} disabled={!hasFileManageFeature} onClick={handleRefresh} />
<Button
title="创建文件夹"
icon={<FolderAddOutlined />}
disabled={!hasFileManageFeature}
onClick={() => {
setModalVisible(true);
}}
/>
<Button
title="导出"
icon={<ExportOutlined />}
disabled={selectRows.length === 0}
onClick={handleExport}
loading={exportLoading}
/>
<Button title="删除" icon={<DeleteOutlined />} disabled={selectRows.length === 0} onClick={handleBatchDelete} />
</Space>
<Breadcrumb style={{ marginBottom: 10 }}>
<Breadcrumb.Item>
<a onClick={() => handlePathClick(-1)}>全部文件</a>
</Breadcrumb.Item>
{dirPath.map((item, index) => (
<Breadcrumb.Item key={item}>
<a onClick={() => handlePathClick(index)}>{item}</a>
</Breadcrumb.Item>
))}
</Breadcrumb>
<div className="table-wrapper">
<Table
size="small"
rowKey="name"
scroll={{ y: windowHeight - 270 }}
dataSource={list}
columns={columns}
pagination={{
size: 'small',
defaultPageSize: 100,
pageSizeOptions: ['50', '100', '200'],
hideOnSinglePage: true
}}
loading={listLoading}
rowSelection={{
onChange: handleTableRowChange,
selectedRowKeys: selectRowKeys,
getCheckboxProps: record => ({ disabled: record?.type === 'directory' })
}}
/>
</div>
<input ref={uploadRef} type="file" multiple hidden onChange={handleFileUpload} />
<Modal
title="创建目录"
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleCreateDirectory}
destroyOnClose={true}
>
<Form form={form} preserve={false}>
<Form.Item
label="目录名称"
name="directoryPath"
rules={[{ required: true }, { pattern: domainPathRegExp, message: '目录名不能以 / 开头或结尾' }]}
>
<Input autoFocus />
</Form.Item>
</Form>
</Modal>
</div>
);
}
Example #27
Source File: index.tsx From Aragorn with MIT License | 4 votes |
Dashboard = () => {
const {
state: {
uploaderProfiles,
configuration: { defaultUploaderProfileId },
uploadedFiles
}
} = useAppContext();
const history = useHistory();
const [selectRowKeys, setRowKeys] = useState([]);
const [selectRows, setSelectRows] = useState([] as UploadedFileInfo[]);
const handleProfileAdd = () => {
history.push('/uploader');
};
const handleProfileClick = id => {
if (id === defaultUploaderProfileId) {
history.push(`/profile/${id}`);
} else {
ipcRenderer.send('set-default-uploader-profile', id);
}
};
const handleCopy = url => {
clipboard.writeText(url);
message.success('已复制到粘贴板');
};
const handleOpen = path => {
shell.showItemInFolder(path);
};
const handleTableRowChange = (selectedRowKeys, selectedRows) => {
setRowKeys(selectedRowKeys);
setSelectRows(selectedRows);
};
const handleClear = () => {
const ids = selectRows.map(item => item.id);
ipcRenderer.send('clear-upload-history', ids);
setRowKeys([]);
};
const handleReUpload = () => {
const data = selectRows.map(item => {
return { id: item.uploaderProfileId, path: item.path };
});
ipcRenderer.send('file-reupload', data);
setRowKeys([]);
};
const columns: ColumnsType<UploadedFileInfo> = [
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
render: (val, record) => (
<Popover
placement="topLeft"
content={() =>
/(jpg|png|gif|jpeg)$/.test(val) ? (
<Image
style={{ maxWidth: 500 }}
src={record.url}
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="
/>
) : (
val
)
}
trigger="hover"
>
<span onClick={() => handleCopy(record.url)} className="row-item">
{val}
</span>
</Popover>
)
},
{
title: '类型',
dataIndex: 'type',
ellipsis: true,
width: 120
},
{
title: '上传器配置',
dataIndex: 'uploaderProfileId',
ellipsis: true,
width: 120,
render: val => (
<a onClick={() => handleProfileClick(val)}>
{uploaderProfiles.find(item => item.id === val)?.name || '未找到'}
</a>
)
},
{
title: '状态',
dataIndex: 'url',
ellipsis: true,
width: 80,
render: val => (
<>
<Badge status={val ? 'success' : 'error'} />
{val ? '成功' : '失败'}
</>
)
},
{
title: '上传时间',
dataIndex: 'date',
width: 180,
ellipsis: true,
render: val => dayjs(val).format('YYYY-MM-DD HH:mm:ss')
},
{
title: '操作',
width: 80,
render: (_, record) => (
<Space>
<FolderOpenOutlined onClick={() => handleOpen(record.path)} />
<CopyOutlined onClick={() => handleCopy(record.url)} />
</Space>
)
}
];
return (
<div className="dashboard-page">
<header>
<span>控制台</span>
<Divider />
</header>
<main>
<div className="profile-wrapper">
<div className="title">上传器配置</div>
<div className="card-wrapper">
{uploaderProfiles.map(item => (
<div
key={item.id}
className={item.id === defaultUploaderProfileId ? 'card card-active' : 'card'}
onClick={() => handleProfileClick(item.id)}
>
<Box className="card-icon" />
<span>{item.name}</span>
</div>
))}
<div className="card" onClick={handleProfileAdd}>
<Plus className="card-icon" />
</div>
</div>
</div>
<div className="history-wrapper">
<div className="title">最近上传</div>
<div className="card-wrapper">
{selectRowKeys.length > 0 && (
<Space style={{ marginBottom: 10 }}>
<Button icon={<DeleteOutlined />} onClick={handleClear}>
清除
</Button>
<Button icon={<UploadOutlined />} onClick={handleReUpload}>
重新上传
</Button>
</Space>
)}
<Table
size="small"
rowKey="id"
dataSource={uploadedFiles}
columns={columns}
rowSelection={{ onChange: handleTableRowChange, selectedRowKeys: selectRowKeys }}
/>
</div>
</div>
</main>
</div>
);
}
Example #28
Source File: ipc-events.ts From WowUp with GNU General Public License v3.0 | 4 votes |
export function initializeIpcHandlers(window: BrowserWindow): void {
log.info("process.versions", process.versions);
ipcMain.on("webview-error", (evt, err, msg) => {
log.error("webview-error", err, msg);
});
// Just forward the token event out to the window
// this is not a handler, just a passive listener
ipcMain.on("wago-token-received", (evt, token) => {
window?.webContents?.send("wago-token-received", token);
});
// Remove the pending URLs once read so they are only able to be gotten once
handle(IPC_GET_PENDING_OPEN_URLS, (): string[] => {
const urls = PENDING_OPEN_URLS;
PENDING_OPEN_URLS = [];
return urls;
});
handle(
IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT,
(
_evt,
key: string,
type: "string" | "boolean" | "integer" | "float" | "double" | "url" | "array" | "dictionary"
) => {
return systemPreferences.getUserDefault(key, type);
}
);
handle("clipboard-read-text", () => {
return clipboard.readText();
});
handle(IPC_SHOW_DIRECTORY, async (evt, filePath: string): Promise<string> => {
return await shell.openPath(filePath);
});
handle(IPC_GET_ASSET_FILE_PATH, (evt, fileName: string) => {
return path.join(__dirname, "..", "assets", fileName);
});
handle(IPC_CREATE_DIRECTORY_CHANNEL, async (evt, directoryPath: string): Promise<boolean> => {
log.info(`[CreateDirectory] '${directoryPath}'`);
await fsp.mkdir(directoryPath, { recursive: true });
return true;
});
handle(IPC_GET_ZOOM_FACTOR, () => {
return window?.webContents?.getZoomFactor();
});
handle(IPC_UPDATE_APP_BADGE, (evt, count: number) => {
return app.setBadgeCount(count);
});
handle(IPC_SET_ZOOM_LIMITS, (evt, minimumLevel: number, maximumLevel: number) => {
return window.webContents?.setVisualZoomLevelLimits(minimumLevel, maximumLevel);
});
handle("show-item-in-folder", (evt, path: string) => {
shell.showItemInFolder(path);
});
handle(IPC_SET_ZOOM_FACTOR, (evt, zoomFactor: number) => {
if (window?.webContents) {
window.webContents.zoomFactor = zoomFactor;
}
});
handle(IPC_ADDONS_SAVE_ALL, (evt, addons: Addon[]) => {
if (!Array.isArray(addons)) {
return;
}
for (const addon of addons) {
addonStore.set(addon.id, addon);
}
});
handle(IPC_GET_APP_VERSION, () => {
return app.getVersion();
});
handle(IPC_GET_LOCALE, () => {
return `${app.getLocale()}`;
});
handle(IPC_GET_LAUNCH_ARGS, () => {
return process.argv;
});
handle(IPC_GET_LOGIN_ITEM_SETTINGS, () => {
return app.getLoginItemSettings();
});
handle(IPC_SET_LOGIN_ITEM_SETTINGS, (evt, settings: Settings) => {
return app.setLoginItemSettings(settings);
});
handle(IPC_READDIR, async (evt, dirPath: string): Promise<string[]> => {
return await fsp.readdir(dirPath);
});
handle(IPC_IS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.isDefaultProtocolClient(protocol);
});
handle(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.setAsDefaultProtocolClient(protocol);
});
handle(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.removeAsDefaultProtocolClient(protocol);
});
handle(IPC_LIST_DIRECTORIES_CHANNEL, async (evt, filePath: string, scanSymlinks: boolean) => {
const files = await fsp.readdir(filePath, { withFileTypes: true });
let symlinkNames: string[] = [];
if (scanSymlinks === true) {
log.info("Scanning symlinks");
const symlinkDirs = await getSymlinkDirs(filePath, files);
symlinkNames = _.map(symlinkDirs, (symLink) => symLink.original.name);
}
const directories = files.filter((file) => file.isDirectory()).map((file) => file.name);
return [...directories, ...symlinkNames];
});
handle(IPC_STAT_FILES_CHANNEL, async (evt, filePaths: string[]) => {
const results: { [path: string]: FsStats } = {};
const taskResults = await firstValueFrom(
from(filePaths).pipe(
mergeMap((filePath) => from(statFile(filePath)), 3),
toArray()
)
);
taskResults.forEach((r) => (results[r.path] = r.fsStats));
return results;
});
handle(IPC_LIST_ENTRIES, async (evt, sourcePath: string, filter: string) => {
const globFilter = globrex(filter);
const results = await fsp.readdir(sourcePath, { withFileTypes: true });
const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
return _.map(matches, (match) => {
const dirEnt: FsDirent = {
isBlockDevice: match.isBlockDevice(),
isCharacterDevice: match.isCharacterDevice(),
isDirectory: match.isDirectory(),
isFIFO: match.isFIFO(),
isFile: match.isFile(),
isSocket: match.isSocket(),
isSymbolicLink: match.isSymbolicLink(),
name: match.name,
};
return dirEnt;
});
});
handle(IPC_LIST_FILES_CHANNEL, async (evt, sourcePath: string, filter: string) => {
const pathExists = await exists(sourcePath);
if (!pathExists) {
return [];
}
const globFilter = globrex(filter);
const results = await fsp.readdir(sourcePath, { withFileTypes: true });
const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
return _.map(matches, (match) => match.name);
});
handle(IPC_PATH_EXISTS_CHANNEL, async (evt, filePath: string) => {
if (!filePath) {
return false;
}
try {
await fsp.access(filePath);
} catch (e) {
if (e.code !== "ENOENT") {
log.error(e);
}
return false;
}
return true;
});
handle(IPC_WOWUP_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise<WowUpScanResult[]> => {
const taskResults = await firstValueFrom(
from(filePaths).pipe(
mergeMap((folder) => from(new WowUpFolderScanner(folder).scanFolder()), 3),
toArray()
)
);
return taskResults;
});
handle(IPC_UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => {
await new Promise((resolve, reject) => {
yauzl.open(arg.zipFilePath, { lazyEntries: true }, (err, zipfile) => {
handleZipFile(err, zipfile, arg.outputFolder).then(resolve).catch(reject);
});
});
await chmodDir(arg.outputFolder, DEFAULT_FILE_MODE);
return arg.outputFolder;
});
handle("zip-file", async (evt, srcPath: string, destPath: string) => {
log.info(`[ZipFile]: '${srcPath} -> ${destPath}`);
return await zipFile(srcPath, destPath);
});
handle("zip-read-file", async (evt, zipPath: string, filePath: string) => {
log.info(`[ZipReadFile]: '${zipPath} : ${filePath}`);
return await readFileInZip(zipPath, filePath);
});
handle("zip-list-files", (evt, zipPath: string, filter: string) => {
log.info(`[ZipListEntries]: '${zipPath}`);
return listZipFiles(zipPath, filter);
});
handle("rename-file", async (evt, srcPath: string, destPath: string) => {
log.info(`[RenameFile]: '${srcPath} -> ${destPath}`);
return await fsp.rename(srcPath, destPath);
});
handle("base64-encode", (evt, content: string) => {
const buff = Buffer.from(content);
return buff.toString("base64");
});
handle("base64-decode", (evt, content: string) => {
const buff = Buffer.from(content, "base64");
return buff.toString("utf-8");
});
handle(IPC_COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest): Promise<boolean> => {
log.info(`[FileCopy] '${arg.sourceFilePath}' -> '${arg.destinationFilePath}'`);
const stat = await fsp.lstat(arg.sourceFilePath);
if (stat.isDirectory()) {
await copyDir(arg.sourceFilePath, arg.destinationFilePath);
await chmodDir(arg.destinationFilePath, DEFAULT_FILE_MODE);
} else {
await fsp.copyFile(arg.sourceFilePath, arg.destinationFilePath);
await fsp.chmod(arg.destinationFilePath, DEFAULT_FILE_MODE);
}
return true;
});
handle(IPC_DELETE_DIRECTORY_CHANNEL, async (evt, filePath: string) => {
log.info(`[FileRemove] ${filePath}`);
return await remove(filePath);
});
handle(IPC_READ_FILE_CHANNEL, async (evt, filePath: string) => {
return await fsp.readFile(filePath, { encoding: "utf-8" });
});
handle(IPC_READ_FILE_BUFFER_CHANNEL, async (evt, filePath: string) => {
return await fsp.readFile(filePath);
});
handle("decode-product-db", async (evt, filePath: string) => {
const productDbData = await fsp.readFile(filePath);
const productDb = ProductDb.decode(productDbData);
setImmediate(() => {
console.log("productDb", JSON.stringify(productDb));
});
return productDb;
});
handle(IPC_WRITE_FILE_CHANNEL, async (evt, filePath: string, contents: string) => {
return await fsp.writeFile(filePath, contents, { encoding: "utf-8", mode: DEFAULT_FILE_MODE });
});
handle(IPC_CREATE_TRAY_MENU_CHANNEL, (evt, config: SystemTrayConfig) => {
return createTray(window, config);
});
handle(IPC_CREATE_APP_MENU_CHANNEL, (evt, config: MenuConfig) => {
return createAppMenu(window, config);
});
handle(IPC_GET_LATEST_DIR_UPDATE_TIME, (evt, dirPath: string) => {
return getLastModifiedFileDate(dirPath);
});
handle(IPC_LIST_DIR_RECURSIVE, (evt, dirPath: string): Promise<string[]> => {
return readDirRecursive(dirPath);
});
handle(IPC_GET_DIRECTORY_TREE, (evt, args: GetDirectoryTreeRequest): Promise<TreeNode> => {
log.debug(IPC_GET_DIRECTORY_TREE, args);
return getDirTree(args.dirPath, args.opts);
});
handle(IPC_GET_HOME_DIR, (): string => {
return os.homedir();
});
handle(IPC_MINIMIZE_WINDOW, () => {
if (window?.minimizable) {
window.minimize();
}
});
handle(IPC_MAXIMIZE_WINDOW, () => {
if (window?.maximizable) {
if (window.isMaximized()) {
window.unmaximize();
} else {
window.maximize();
}
}
});
handle(IPC_CLOSE_WINDOW, () => {
window?.close();
});
handle(IPC_FOCUS_WINDOW, () => {
restoreWindow(window);
window?.focus();
});
handle(IPC_RESTART_APP, () => {
log.info(`[RestartApp]`);
app.relaunch();
app.quit();
});
handle(IPC_QUIT_APP, () => {
log.info(`[QuitApp]`);
app.quit();
});
handle(IPC_LIST_DISKS_WIN32, async () => {
const diskInfos = await nodeDiskInfo.getDiskInfo();
// Cant pass complex objects over the wire, make them simple
return diskInfos.map((di) => {
return {
mounted: di.mounted,
filesystem: di.filesystem,
};
});
});
handle(IPC_WINDOW_LEAVE_FULLSCREEN, () => {
window?.setFullScreen(false);
});
handle(IPC_SHOW_OPEN_DIALOG, async (evt, options: OpenDialogOptions) => {
return await dialog.showOpenDialog(options);
});
handle(IPC_PUSH_INIT, () => {
return push.startPushService();
});
handle(IPC_PUSH_REGISTER, async (evt, appId: string) => {
return await push.registerForPush(appId);
});
handle(IPC_PUSH_UNREGISTER, async () => {
return await push.unregisterPush();
});
handle(IPC_PUSH_SUBSCRIBE, async (evt, channel: string) => {
return await push.subscribeToChannel(channel);
});
handle("get-focus", () => {
return window.isFocused();
});
ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => {
handleDownloadFile(arg).catch((e) => log.error(e.toString()));
});
// In order to allow concurrent downloads, we have to get creative with this session handler
window.webContents.session.on("will-download", (evt, item, wc) => {
for (const key of _dlMap.keys()) {
log.info(`will-download: ${key}`);
if (!item.getURLChain().includes(key)) {
continue;
}
try {
const action = _dlMap.get(key);
action.call(null, evt, item, wc);
} catch (e) {
log.error(e);
} finally {
_dlMap.delete(key);
}
}
});
async function statFile(filePath: string) {
const stats = await fsp.stat(filePath);
const fsStats: FsStats = {
atime: stats.atime,
atimeMs: stats.atimeMs,
birthtime: stats.birthtime,
birthtimeMs: stats.birthtimeMs,
blksize: stats.blksize,
blocks: stats.blocks,
ctime: stats.ctime,
ctimeMs: stats.ctimeMs,
dev: stats.dev,
gid: stats.gid,
ino: stats.ino,
isBlockDevice: stats.isBlockDevice(),
isCharacterDevice: stats.isCharacterDevice(),
isDirectory: stats.isDirectory(),
isFIFO: stats.isFIFO(),
isFile: stats.isFile(),
isSocket: stats.isSocket(),
isSymbolicLink: stats.isSymbolicLink(),
mode: stats.mode,
mtime: stats.mtime,
mtimeMs: stats.mtimeMs,
nlink: stats.nlink,
rdev: stats.rdev,
size: stats.size,
uid: stats.uid,
};
return { path: filePath, fsStats };
}
async function handleDownloadFile(arg: DownloadRequest) {
const status: DownloadStatus = {
type: DownloadStatusType.Pending,
savePath: "",
};
try {
await fsp.mkdir(arg.outputFolder, { recursive: true });
const downloadUrl = new URL(arg.url);
if (typeof arg.auth?.queryParams === "object") {
for (const [key, value] of Object.entries(arg.auth.queryParams)) {
downloadUrl.searchParams.set(key, value);
}
}
const savePath = path.join(arg.outputFolder, `${nanoid()}-${arg.fileName}`);
log.info(`[DownloadFile] '${downloadUrl.toString()}' -> '${savePath}'`);
const url = downloadUrl.toString();
const writer = fs.createWriteStream(savePath);
try {
await new Promise((resolve, reject) => {
let size = 0;
let percentMod = -1;
const req = net.request({
url,
redirect: "manual",
});
if (typeof arg.auth?.headers === "object") {
for (const [key, value] of Object.entries(arg.auth.headers)) {
log.info(`Setting header: ${key}=${value}`);
req.setHeader(key, value);
}
}
req.on("redirect", (status, method, redirectUrl) => {
log.info(`[download] caught redirect`, status, redirectUrl);
req.followRedirect();
});
req.on("response", (response) => {
const fileLength = parseInt((response.headers["content-length"] as string) ?? "0", 10);
response.on("data", (data) => {
writer.write(data, () => {
size += data.length;
const percent = fileLength <= 0 ? 0 : Math.floor((size / fileLength) * 100);
if (percent % 5 === 0 && percentMod !== percent) {
percentMod = percent;
log.debug(`Write: [${percent}] ${size}`);
}
});
});
response.on("end", () => {
if (response.statusCode < 200 || response.statusCode >= 300) {
return reject(new Error(`Invalid response (${response.statusCode}): ${url}`));
}
return resolve(undefined);
});
response.on("error", (err) => {
return reject(err);
});
});
req.end();
});
} finally {
// always close stream
writer.end();
}
status.type = DownloadStatusType.Complete;
status.savePath = savePath;
window.webContents.send(arg.responseKey, status);
} catch (err) {
log.error(err);
status.type = DownloadStatusType.Error;
status.error = err;
window.webContents.send(arg.responseKey, status);
}
}
}