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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
   * 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 vote down vote up
/**
   * 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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
async writeToClipboard(text: string) {
    clipboard.writeText(text, 'clipboard');
    return true;
  }
Example #12
Source File: uploaderManager.ts    From Aragorn with MIT License 5 votes vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
 * @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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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,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"
              />
            ) : (
              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 vote down vote up
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);
    }
  }
}