1
0
Fork 0
mirror of https://github.com/anyproto/anytype-ts.git synced 2025-06-08 05:57:02 +09:00
This commit is contained in:
Andrew Simachev 2024-01-31 22:14:56 +01:00
commit 6703b2c8ca
No known key found for this signature in database
GPG key ID: 49A163D0D14E6FD8
125 changed files with 3068 additions and 304 deletions

View file

@ -11,8 +11,8 @@ jobs:
strategy:
matrix:
go-version: [ 1.16.x ]
os: [ macos-latest, ubuntu-latest, windows-latest ]
go-version: [ 1.18.10 ]
os: [ macos-latest, ubuntu-latest, windows-latest ]
steps:
- name: Setup
@ -59,6 +59,14 @@ jobs:
./update-ci.sh ${{secrets.USER}} ${{secrets.TOKEN}} ${{matrix.os}} arm
./update-ci.sh ${{secrets.USER}} ${{secrets.TOKEN}} ${{matrix.os}} amd
- name: Build Native Messaging Host Windows
if: matrix.os == 'windows-latest'
run: npm run build:nmh-win
- name: Build Native Messaging Host
if: matrix.os != 'windows-latest'
run: npm run build:nmh
- name: Build Front Mac OS
if: matrix.os == 'macos-latest'
uses: samuelmeuli/action-electron-builder@v1
@ -109,6 +117,7 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
rm dist/anytypeHelper.exe
rm dist/nativeMessagingHost.exe
mv dist/*.exe artifacts
- name: Release

30
.github/workflows/extension.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: Test
on:
push:
tags:
- '*'
jobs:
build:
name: Publish webextension
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Build
run: |
npm ci
npm run build
- name: Upload & release
uses: mnao305/chrome-extension-upload@v4.0.1
with:
file-path: dist/file.zip
extension-id: hogefuga(extension id)
client-id: ${{ secrets.CLIENT_ID }}
client-secret: ${{ secrets.CLIENT_SECRET }}
refresh-token: ${{ secrets.REFRESH_TOKEN }}
publish: false

View file

@ -5,7 +5,7 @@ npx lint-staged --concurrent false
# npm run typecheck
# Checking for secrets
gitleaks protect --verbose --redact --staged
#gitleaks protect --verbose --redact --staged
# Checking dependencies' licenses
npx license-checker --production --json --out licenses.json

11
dist/.gitignore vendored
View file

@ -7,6 +7,12 @@ main.js
main.js.map
commands.js
bundle-back.js
anytypeHelper*
nativeMessagingHost*
alpha-linux.yml
beta-linux.yml
builder-debug.yml
latest-linux.yml
*-arm64/
*-unpacked/
*.snap
@ -17,3 +23,8 @@ bundle-back.js
*.node
*.yml
*.yaml
nmh.log
[0-9]*.js
[0-9]*.js.map
extension.crx
extension.pem

73
dist/challenge/index.html vendored Normal file
View file

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
* { margin: 0px; }
html, body { height: 100%; }
html.dark body { background-color: #171717; color: #a09f92; }
body { font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 22px; padding: 16px; background-color: #fff; color: #252525; }
.content { display: flex; align-items: center; justify-content: center; flex-direction: column; }
.logo { width: 64px; height: 64px; -webkit-user-select: none; user-select: none; margin: 0px 0px 12px 0px; }
.logo img { width: 100% !important; height: 100% !important; }
.title { font-size: 22px; line-height: 28px; letter-spacing: -0.48px; font-weight: 700; margin: 0px 0px 24px 0px; }
.buttons button { -webkit-app-region: no-drag; }
.buttons { margin-bottom: 1em; text-align: center; }
.buttons button {
display: inline-block; text-align: center; border: 0px; font-weight: 500; text-decoration: none;
height: 30px; line-height: 30px; padding: 0px 16px; border-radius: 4px; transition: background 0.2s ease-in-out;
font-size: 14px; vertical-align: middle; position: relative; overflow: hidden; letter-spacing: 0.2px;
background: #ffb522; color: #fff; margin-top: 1em; width: 100px;
}
.buttons button:hover { background: #f09c0e; }
</style>
<script src="../js/jquery.js" type="text/javascript"></script>
</head>
<body>
<div class="content">
<div class="logo">
<img src="../img/icon/app/64x64.png" />
</div>
<div class="title">Challenge</div>
<div id="challenge"></div>
<div class="buttons">
<button id="close"></button>
</div>
</div>
<script type="text/javascript">
$(() => {
const win = $(window);
const closeButton = $('#close');
const challengeEl = $('#challenge');
document.title = 'Anytype';
closeButton.off('click').on('click', e => {
e.preventDefault();
window.close();
});
win.off('message').on('message', e => {
const { challenge, theme, lang } = e.originalEvent.data;
challengeEl.text(challenge);
$('html').attr({ class: theme });
$.ajax({
url: `../lib/json/lang/${lang}.json`,
method: 'GET',
contentType: 'application/json',
success: data => {
closeButton.text(data.commonClose);
},
});
});
});
</script>
</body>
</html>

10
dist/extension/css/foreground.css vendored Normal file
View file

@ -0,0 +1,10 @@
#anytypeWebclipper-container { position: fixed; z-index: 100000; width: 100%; height: 100%; left: 0px; top: 0px; display: none; }
#anytypeWebclipper-iframe {
position: absolute; z-index: 1; width: 800px; height: 600px; background: #fff; left: 50%; top: 50%; margin: -300px 0px 0px -400px;
border-radius: 16px; border: 1px solid rgba(172, 169, 152, 0.12); box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.10);
}
#anytypeWebclipper-container .dimmer {
position: absolute; z-index: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); left: 0px; top: 0px;
}

10
dist/extension/iframe/index.html vendored Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Anytype Web Clipper</title>
</head>
<body>
<script type="text/javascript" src="../js/main.js"></script>
</body>
</html>

BIN
dist/extension/img/icon128x128.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
dist/extension/img/icon16x16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

BIN
dist/extension/img/icon32x32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

2
dist/extension/js/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
main.js
main.js.map

154
dist/extension/js/background.js vendored Normal file
View file

@ -0,0 +1,154 @@
(() => {
let ports = [];
let isInitMenu = false;
const native = chrome.runtime.connectNative('com.anytype.desktop');
native.postMessage({ type: 'getPorts' });
native.onMessage.addListener((msg) => {
console.log('[Native]', msg);
if (msg.error) {
console.error(msg.error);
};
switch (msg.type) {
case 'launchApp': {
sendToActiveTab({ type: 'launchApp', res: msg.response });
break;
};
case 'getPorts': {
if (msg.response) {
for (let pid in msg.response) {
ports = msg.response[pid];
break;
};
};
break;
};
};
});
native.onDisconnect.addListener(() => {
console.log('[Native] Disconnected');
});
chrome.runtime.onInstalled.addListener(details => {
if (![ 'install', 'update' ].includes(details.reason)) {
return;
};
if (details.reason == 'update') {
const { version } = chrome.runtime.getManifest();
console.log('Updated', details.previousVersion, version);
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
getActiveTab(tab => {
if (tab && (tabId == tab.id) && (undefined !== changeInfo.url)) {
sendToTab(tab, { type: 'hide' });
};
});
});
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log('[Background]', msg.type);
const res = {};
switch (msg.type) {
case 'launchApp': {
native.postMessage({ type: 'launchApp' });
break;
};
case 'getPorts': {
native.postMessage({ type: 'getPorts' });
break;
};
case 'checkPorts': {
res.ports = ports;
break;
};
case 'init': {
initMenu();
break;
};
};
sendResponse(res);
return true;
});
initMenu = async () => {
if (isInitMenu) {
return;
};
isInitMenu = true;
chrome.contextMenus.create({
id: 'webclipper',
title: 'Anytype Web Clipper',
contexts: [ 'selection' ]
});
chrome.contextMenus.onClicked.addListener(async () => {
const tab = await getActiveTab();
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: () => {
const sel = window.getSelection();
let html = '';
if (sel.rangeCount) {
const container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
};
html = container.innerHTML;
};
return html;
}
}, res => {
if (res.length) {
sendToTab(tab, { type: 'clickMenu', html: res[0].result });
};
});
});
};
getActiveTab = async () => {
const [ tab ] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
return tab;
};
sendToActiveTab = async (msg) => {
const tab = await getActiveTab();
console.log('[sendToActiveTab]', tab, msg);
await sendToTab(tab, msg);
};
sendToTab = async (tab, msg) => {
if (!tab) {
return;
};
const response = await chrome.tabs.sendMessage(tab.id, msg);
console.log('[sendToTab]', tab, msg, response);
};
})();

59
dist/extension/js/foreground.js vendored Normal file
View file

@ -0,0 +1,59 @@
(() => {
const extensionId = 'jkmhmgghdjjbafmkgjmplhemjjnkligf';
const body = document.querySelector('body');
const container = document.createElement('div');
const dimmer = document.createElement('div');
const iframe = document.createElement('iframe');
if (body && !document.getElementById(iframe.id)) {
body.appendChild(container);
};
container.id = [ 'anytypeWebclipper', 'container' ].join('-');
container.appendChild(iframe);
container.appendChild(dimmer);
iframe.id = [ 'anytypeWebclipper', 'iframe' ].join('-');
iframe.src = chrome.runtime.getURL('iframe/index.html');
dimmer.className = 'dimmer';
dimmer.addEventListener('click', () => {
container.style.display = 'none';
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log('[Foreground]', msg, sender);
if (sender.id != extensionId) {
return false;
};
switch (msg.type) {
case 'clickMenu':
container.style.display = 'block';
break;
case 'hide':
container.style.display = 'none';
break;
};
sendResponse({});
return true;
});
window.addEventListener('message', (e) => {
if (e.origin != `chrome-extension://${extensionId}`) {
return;
};
const { data } = e;
switch (data.type) {
case 'clickClose':
container.style.display = 'none';
break;
};
});
})();

38
dist/extension/manifest.json vendored Normal file
View file

@ -0,0 +1,38 @@
{
"manifest_version": 3,
"name": "Anytype Web Clipper",
"description": "Anytype is a next generation software that breaks down barriers between applications, gives back privacy and data ownership to users",
"version": "0.0.1",
"icons": {
"16": "img/icon16x16.png",
"128": "img/icon128x128.png"
},
"options_page": "settings/index.html",
"action": {
"default_title": "Anytype Web Clipper",
"default_popup": "popup/index.html"
},
"permissions": [
"contextMenus",
"nativeMessaging",
"tabs",
"scripting",
"activeTab"
],
"background": {
"service_worker": "js/background.js"
},
"content_scripts": [
{
"js": [ "js/foreground.js" ],
"css": [ "css/foreground.css" ],
"matches": [ "<all_urls>" ]
}
],
"web_accessible_resources": [
{
"resources": [ "iframe/index.html" ],
"matches": [ "<all_urls>" ]
}
]
}

10
dist/extension/popup/index.html vendored Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Anytype Web Clipper</title>
</head>
<body>
<script type="text/javascript" src="../js/main.js"></script>
</body>
</html>

7
dist/extension/settings/index.html vendored Normal file
View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</body>
</html>

BIN
dist/img/icon/app/1024x1024.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
dist/img/icon/app/128x128.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
dist/img/icon/app/16x16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

BIN
dist/img/icon/app/256x256.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
dist/img/icon/app/32x32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

BIN
dist/img/icon/app/512x512.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
dist/img/icon/app/64x64.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

3
dist/polyfill.js vendored
View file

@ -1,7 +1,6 @@
const RendererEvents = {};
window.Anytype = window.Anytype || {};
window.Anytype.Config = {
window.AnytypeGlobalConfig = {
debug: {
mw: true,
},

View file

@ -7,6 +7,7 @@ const storage = require('electron-json-storage');
const port = process.env.SERVER_PORT;
const protocol = 'anytype';
const remote = require('@electron/remote/main');
const { installNativeMessagingHost } = require('./electron/js/lib/installNativeMessagingHost.js');
const binPath = fixPathForAsarUnpack(path.join(__dirname, 'dist', `anytypeHelper${is.windows ? '.exe' : ''}`));
// Fix notifications app name
@ -129,6 +130,8 @@ function createWindow () {
MenuManager.initMenu();
MenuManager.initTray();
installNativeMessagingHost();
ipcMain.removeHandler('Api');
ipcMain.handle('Api', (e, id, cmd, args) => {
const Api = require('./electron/js/api.js');
@ -167,18 +170,18 @@ app.on('second-instance', (event, argv) => {
deeplinkingUrl = argv.find((arg) => arg.startsWith(`${protocol}://`));
};
if (mainWindow) {
if (deeplinkingUrl) {
Util.send(mainWindow, 'route', Util.getRouteFromUrl(deeplinkingUrl));
};
if (mainWindow.isMinimized()) {
mainWindow.restore();
};
mainWindow.show();
mainWindow.focus();
if (!mainWindow || !deeplinkingUrl) {
return;
};
Util.send(mainWindow, 'route', Util.getRouteFromUrl(deeplinkingUrl));
if (mainWindow.isMinimized()) {
mainWindow.restore();
};
mainWindow.show();
mainWindow.focus();
});
app.on('before-quit', (e) => {

View file

@ -27,8 +27,6 @@ body { margin: 0px; color: #252525; font-family: Helvetica, Arial, sans-serif; f
}
.version .copy.active { background-image: url('../img/check.svg'); }
.buttons { margin-bottom: 1em; text-align: center; }
.buttons button { margin-top: 1em; width: 100px; height: 24px; }
.link { color: #80a0c2; }
.bug-report-link { position: absolute; right: 0.5em; bottom: 0.5em; }
@ -36,13 +34,14 @@ body { margin: 0px; color: #252525; font-family: Helvetica, Arial, sans-serif; f
.bug-report-link,
.buttons button { -webkit-app-region: no-drag; }
.buttons { margin-bottom: 1em; text-align: center; }
.buttons button {
display: inline-block; text-align: center; border: 0px; font-weight: 500; text-decoration: none;
height: 30px; line-height: 30px; padding: 0px 16px; border-radius: 4px; transition: background 0.2s ease-in-out;
font-size: 14px; vertical-align: middle; position: relative; overflow: hidden; letter-spacing: 0.2px;
background: #ffb522; color: #fff;
background: #ffb522; color: #fff; margin-top: 1em; width: 100px;
}
.buttons button:hover { background: #f1981a; }
.buttons button:hover { background: #f09c0e; }
html.dark body { color: #a09f92; }
html.dark .title { color: #dfddd3; }

View file

@ -6,7 +6,7 @@ $(() => {
var versionText = '';
var timeout = 0;
document.title = 'Anytype';
$('html').attr({ class: param.theme });
closeButton.on('click', e => {
e.preventDefault();
@ -46,4 +46,15 @@ $(() => {
},
});
function getParam () {
var a = location.search.replace(/^\?/, '').split('&');
var param = {};
a.forEach((s) => {
var kv = s.split('=');
param[kv[0]] = kv[1];
});
return param;
};
});

View file

@ -1,14 +0,0 @@
function getParam () {
var a = location.search.replace(/^\?/, '').split('&');
var param = {};
a.forEach((s) => {
var kv = s.split('=');
param[kv[0]] = kv[1];
});
return param;
};
var param = getParam();
document.getElementById('html').className = param.theme;

View file

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
<title>Anytype</title>
<script src="./common.js" type="text/javascript"></script>
<script src="../../dist/js/jquery.js" type="text/javascript"></script>
<link rel="stylesheet" href="./about.css" />
</head>

View file

@ -40,7 +40,6 @@ class Api {
languages: win.webContents.session.availableSpellCheckerLanguages,
css: String(css || ''),
});
win.route = '';
};

View file

@ -0,0 +1,177 @@
/*
- This file is responsible for installing the native messaging host manifest file in the correct location for each browser on each platform.
- It is idempotent, meaning it can run multiple times without causing any problems.
- The native messaging host is a small executable that can be called by the Webclipper browser extension.
- the executable remains in the anytype application files, but the manifest file is installed in each browser's unique nativeMessagingHost directory.
- Read about what the actual executable does in the file: go/nativeMessagingHost.go
*/
const { existsSync, mkdir, writeFile } = require('fs');
const { userInfo, homedir } = require('os');
const { app } = require('electron');
const path = require('path');
const util = require('../util.js');
const { fixPathForAsarUnpack, is } = require('electron-util');
const APP_NAME = 'com.anytype.desktop';
const MANIFEST_FILENAME = `${APP_NAME}.json`;
const EXTENSION_ID = 'jkmhmgghdjjbafmkgjmplhemjjnkligf';
const USER_PATH = app.getPath('userData');
const EXE_PATH = app.getPath('exe');
const getManifestPath = () => {
const fn = `nativeMessagingHost${is.windows ? '.exe' : ''}`;
return path.join(fixPathForAsarUnpack(__dirname), '..', '..', '..', 'dist', fn);
};
const getHomeDir = () => {
if (process.platform === 'darwin') {
return userInfo().homedir;
} else {
return homedir();
};
};
const installNativeMessagingHost = () => {
const { platform } = process;
// TODO make sure this is idempotent
const manifest = {
name: APP_NAME,
description: 'Anytype desktop <-> web clipper bridge',
type: 'stdio',
allowed_origins: [ `chrome-extension://${EXTENSION_ID}/` ],
path: getManifestPath(),
};
switch (platform) {
case 'win32': {
installToWindows(manifest);
break;
}
case 'darwin': {
installToMacOS(manifest);
break;
}
case 'linux':
installToLinux(manifest);
break;
default:
console.log('unsupported platform: ', platform);
break;
};
};
const installToMacOS = (manifest) => {
const dirs = getDarwinDirectory();
for (const [ key, value ] of Object.entries(dirs)) {
if (existsSync(value)) {
const p = path.join(value, 'NativeMessagingHosts', MANIFEST_FILENAME);
writeManifest(p, manifest).catch(e => {
console.log(`Error writing manifest for ${key}. ${e}`);
});
} else {
console.log(`Warning: ${key} not found skipping.`);
};
};
};
const installToLinux = (manifest) => {
const dir = `${getHomeDir()}/.config/google-chrome/`;
writeManifest(`${dir}NativeMessagingHosts/${MANIFEST_FILENAME}`, manifest);
};
const installToWindows = (manifest) => {
const dir = path.join(USER_PATH, 'browsers');
writeManifest(path.join(dir, 'chrome.json'), manifest);
createWindowsRegistry(
'HKCU\\SOFTWARE\\Google\\Chrome',
`HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\${APP_NAME}`,
path.join(dir, 'chrome.json')
);
};
const getRegeditInstance = () => {
// eslint-disable-next-line
const regedit = require('regedit');
regedit.setExternalVBSLocation(
path.join(path.dirname(EXE_PATH), 'resources/regedit/vbs')
);
return regedit;
};
const createWindowsRegistry = async (check, location, jsonFile) => {
const regedit = getRegeditInstance();
const list = util.promisify(regedit.list);
const createKey = util.promisify(regedit.createKey);
const putValue = util.promisify(regedit.putValue);
console.log(`Adding registry: ${location}`);
// Check installed
try {
await list(check);
} catch {
console.log(`Not finding registry ${check} skipping.`);
return;
};
try {
await createKey(location);
// Insert path to manifest
const obj = {};
obj[location] = {
default: {
value: jsonFile,
type: 'REG_DEFAULT',
},
};
return putValue(obj);
} catch (error) {
console.log(error);
};
};
const getDarwinDirectory = () => {
const HOME_DIR = getHomeDir();
/* eslint-disable no-useless-escape */
return {
Firefox: `${HOME_DIR}/Library/Application\ Support/Mozilla/`,
Chrome: `${HOME_DIR}/Library/Application\ Support/Google/Chrome/`,
'Chrome Beta': `${HOME_DIR}/Library/Application\ Support/Google/Chrome\ Beta/`,
'Chrome Dev': `${HOME_DIR}/Library/Application\ Support/Google/Chrome\ Dev/`,
'Chrome Canary': `${HOME_DIR}/Library/Application\ Support/Google/Chrome\ Canary/`,
Chromium: `${HOME_DIR}/Library/Application\ Support/Chromium/`,
'Microsoft Edge': `${HOME_DIR}/Library/Application\ Support/Microsoft\ Edge/`,
'Microsoft Edge Beta': `${HOME_DIR}/Library/Application\ Support/Microsoft\ Edge\ Beta/`,
'Microsoft Edge Dev': `${HOME_DIR}/Library/Application\ Support/Microsoft\ Edge\ Dev/`,
'Microsoft Edge Canary': `${HOME_DIR}/Library/Application\ Support/Microsoft\ Edge\ Canary/`,
Vivaldi: `${HOME_DIR}/Library/Application\ Support/Vivaldi/`,
};
/* eslint-enable no-useless-escape */
};
const writeManifest = async (dst, data) => {
if (!existsSync(path.dirname(dst))) {
await mkdir(path.dirname(dst));
};
await writeFile(dst, JSON.stringify(data, null, 2), (err) => {
if (err) {
console.log(err);
} else {
console.log(`Manifest written: ${dst}`);
};
});
};
module.exports = { installNativeMessagingHost };

43
extension/entry.tsx Normal file
View file

@ -0,0 +1,43 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import $ from 'jquery';
import Popup from './popup';
import Iframe from './iframe';
import Util from './lib/util';
import Extension from 'json/extension.json';
import Url from 'json/url.json';
import './scss/common.scss';
window.Electron = {
currentWindow: () => ({ windowId: 1 }),
Api: () => {},
};
window.AnytypeGlobalConfig = { emojiPrefix: Url.emojiPrefix, menuBorderTop: 16, menuBorderBottom: 16, debug: { mw: true } };
let rootId = '';
let component: any = null;
if (Util.isPopup()) {
rootId = `${Extension.clipper.prefix}-popup`;
component = <Popup />;
} else
if (Util.isIframe()) {
rootId = `${Extension.clipper.prefix}-iframe`;
component = <Iframe />;
};
if (!rootId) {
console.error('[Entry] rootId is not defined');
} else {
const html = $('html');
const body = $('body');
const root = $(`<div id="${rootId}"></div>`);
if (!$(`#${rootId}`).length) {
body.append(root);
html.addClass(rootId);
};
ReactDOM.render(component, root.get(0));
};

134
extension/iframe.tsx Normal file
View file

@ -0,0 +1,134 @@
import * as React from 'react';
import * as hs from 'history';
import $ from 'jquery';
import { Router, Route, Switch } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import { Provider } from 'mobx-react';
import { configure } from 'mobx';
import { ListMenu } from 'Component';
import { dispatcher, C, UtilCommon, UtilRouter } from 'Lib';
import { commonStore, authStore, blockStore, detailStore, dbStore, menuStore, popupStore, extensionStore } from 'Store';
import Index from './iframe/index';
import Create from './iframe/create';
import Util from './lib/util';
require('./scss/iframe.scss');
configure({ enforceActions: 'never' });
const Routes = [
{ path: '/' },
{ path: '/:page' },
];
const Components = {
index: Index,
create: Create,
};
const memoryHistory = hs.createMemoryHistory;
const history = memoryHistory();
const rootStore = {
commonStore,
authStore,
blockStore,
detailStore,
dbStore,
menuStore,
popupStore,
extensionStore,
};
declare global {
interface Window {
Electron: any;
$: any;
Anytype: any;
isWebVersion: boolean;
AnytypeGlobalConfig: any;
}
};
window.$ = $;
window.$ = $;
window.Anytype = {
Store: rootStore,
Lib: {
C,
UtilCommon,
dispatcher,
Storage,
},
};
class RoutePage extends React.Component<RouteComponentProps> {
render () {
const { match } = this.props;
const params = match.params as any;
const page = params.page || 'index';
const Component = Components[page];
return (
<React.Fragment>
<ListMenu key="listMenu" {...this.props} />
{Component ? <Component /> : null}
</React.Fragment>
);
};
};
class Iframe extends React.Component {
node: any = null;
render () {
return (
<Router history={history}>
<Provider {...rootStore}>
<div ref={node => this.node = node}>
<Switch>
{Routes.map((item: any, i: number) => (
<Route path={item.path} exact={true} key={i} component={RoutePage} />
))}
</Switch>
</div>
</Provider>
</Router>
);
};
componentDidMount () {
UtilRouter.init(history);
/* @ts-ignore */
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log('[Iframe]', msg, sender);
switch (msg.type) {
case 'init':
const { appKey, gatewayPort, serverPort } = msg;
Util.init(serverPort, gatewayPort);
Util.authorize(appKey, () => UtilRouter.go('/create', {}));
sendResponse({});
break;
case 'clickMenu': {
extensionStore.setHtml(msg.html);
sendResponse({});
break;
};
};
return true;
});
};
};
export default Iframe;

185
extension/iframe/create.tsx Normal file
View file

@ -0,0 +1,185 @@
import * as React from 'react';
import $ from 'jquery';
import { observer } from 'mobx-react';
import { Button, Block, Loader, Icon, Select } from 'Component';
import { I, C, M, translate, UtilObject, UtilData } from 'Lib';
import { blockStore, extensionStore, menuStore, dbStore, commonStore } from 'Store';
interface State {
isLoading: boolean;
error: string;
object: any;
};
const ROOT_ID = 'preview';
const Create = observer(class Create extends React.Component<I.PageComponent, State> {
state: State = {
isLoading: false,
error: '',
object: null,
};
node: any = null;
refSpace: any = null;
html = '';
constructor (props: I.PageComponent) {
super(props);
this.onSave = this.onSave.bind(this);
this.onClose = this.onClose.bind(this);
this.onSelect = this.onSelect.bind(this);
this.onSpaceChange = this.onSpaceChange.bind(this);
};
render () {
const { isLoading, error, object } = this.state;
const { html } = extensionStore;
const { space } = commonStore;
const children = blockStore.getChildren(ROOT_ID, ROOT_ID);
return (
<div ref={ref => this.node = ref} className="page pageCreate">
{isLoading ? <Loader type="loader" /> : ''}
<div className="head">
<div className="side left">
<Select
id="select-space"
ref={ref => this.refSpace = ref}
value=""
options={[]}
onChange={this.onSpaceChange}
menuParam={{
horizontal: I.MenuDirection.Center,
data: { maxHeight: 360 }
}}
/>
<div id="select" className="select" onMouseDown={this.onSelect}>
<div className="item">
<div className="name">{object ? object.name : translate('commonSelectObject')}</div>
</div>
<Icon className="arrow light" />
</div>
</div>
<div className="side right">
<Button text="Cancel" color="blank" className="c32" onClick={this.onClose} />
<Button text="Save" color="pink" className="c32" onClick={this.onSave} />
</div>
</div>
<div className="blocks">
{children.map((block: I.Block, i: number) => (
<Block
key={block.id}
{...this.props}
rootId={ROOT_ID}
index={i}
block={block}
getWrapperWidth={() => this.getWrapperWidth()}
readonly={true}
/>
))}
</div>
</div>
);
};
componentDidMount(): void {
const spaces = dbStore.getSpaces().map(it => ({ ...it, id: it.targetSpaceId, object: it, iconSize: 16 })).filter(it => it);
if (this.refSpace && spaces.length) {
const space = commonStore.space || spaces[0].targetSpaceId;
this.refSpace?.setOptions(spaces);
this.refSpace?.setValue(space);
this.onSpaceChange(space);
};
};
componentDidUpdate (): void {
this.initBlocks();
};
initBlocks () {
const { html } = extensionStore;
if (!html || (html == this.html)) {
return;
};
this.html = html;
C.BlockPreview(html, (message: any) => {
if (message.error.code) {
return;
};
const structure: any[] = [];
const blocks = message.blocks.map(it => new M.Block(it));
blocks.forEach((block: any) => {
structure.push({ id: block.id, childrenIds: block.childrenIds });
});
blockStore.set(ROOT_ID, blocks);
blockStore.setStructure(ROOT_ID, structure);
this.forceUpdate();
});
};
onSelect () {
const { object } = this.state;
const node = $(this.node);
const filters: I.Filter[] = [
{ operator: I.FilterOperator.And, relationKey: 'layout', condition: I.FilterCondition.In, value: UtilObject.getPageLayouts() },
];
menuStore.open('searchObject', {
element: node.find('#select'),
data: {
value: object ? [ object.id ] : [],
canAdd: true,
filters,
details: { origin: I.ObjectOrigin.Webclipper },
dataMapper: item => ({ ...item, iconSize: 16 }),
onSelect: (item) => {
this.setState({ object: item });
},
}
});
};
onSpaceChange (id: string): void {
commonStore.spaceSet(id);
UtilData.createsSubscriptions();
};
getWrapperWidth () {
const win: any = $(window);
return win.width() - 96;
};
onSave () {
const { object } = this.state;
if (!object) {
return;
};
C.BlockPaste (object.id, '', { from: 0, to: 0 }, [], false, { html: this.html }, () => {
this.onClose();
});
};
onClose () {
this.html = '';
parent.postMessage({ type: 'clickClose' }, '*');
};
});
export default Create;

View file

@ -0,0 +1,46 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { I, UtilRouter, Storage } from 'Lib';
import Util from '../lib/util';
const Index = observer(class Index extends React.Component<I.PageComponent> {
render () {
return (
<div className="page pageIndex" />
);
};
componentDidMount(): void {
this.getPorts();
};
getPorts (onError?: () => void): void {
Util.sendMessage({ type: 'checkPorts' }, response => {
console.log('[Iframe] checkPorts', response);
if (!response.ports || !response.ports.length) {
this.setState({ error: 'Automatic pairing failed, please open the app' });
if (onError) {
onError();
};
return;
};
Util.init(response.ports[1], response.ports[2]);
this.login();
});
};
login () {
const appKey = Storage.get('appKey');
if (appKey) {
Util.authorize(appKey, () => UtilRouter.go('/create', {}), () => Storage.delete('appKey'));
};
};
});
export default Index;

78
extension/lib/util.ts Normal file
View file

@ -0,0 +1,78 @@
import { UtilData, dispatcher } from 'Lib';
import { authStore, commonStore, extensionStore } from 'Store';
import Extension from 'json/extension.json';
const INDEX_POPUP = '/popup/index.html';
const INDEX_IFRAME = '/iframe/index.html'
class Util {
extensionId () {
return Extension.clipper.id;
};
isExtension () {
return (
(location.protocol == 'chrome-extension:') &&
(location.hostname == this.extensionId())
);
};
isPopup () {
return this.isExtension() && (location.pathname == INDEX_POPUP);
};
isIframe () {
return this.isExtension() && (location.pathname == INDEX_IFRAME);
};
fromPopup (url: string) {
return url.match(INDEX_POPUP);
};
fromIframe (url: string) {
return url.match(INDEX_IFRAME);
};
sendMessage (msg: any, callBack: (response) => void) {
/* @ts-ignore */
chrome.runtime.sendMessage(msg, callBack);
};
getCurrentTab (callBack: (tab) => void) {
/* @ts-ignore */
chrome.tabs.query({ active: true, lastFocusedWindow: true }, tabs => callBack(tabs[0]));
};
init (serverPort: string, gatewayPort: string) {
extensionStore.serverPort = serverPort;
extensionStore.gatewayPort = gatewayPort;
dispatcher.init(`http://127.0.0.1:${serverPort}`);
commonStore.gatewaySet(`http://127.0.0.1:${gatewayPort}`);
};
authorize (appKey: string, onSuccess?: () => void, onError?: (error) => void) {
const { serverPort, gatewayPort } = extensionStore;
authStore.appKeySet(appKey);
UtilData.createSession((message: any) => {
if (message.error.code) {
if (onError) {
onError(message.error);
};
return;
};
this.sendMessage({ type: 'init', appKey, serverPort, gatewayPort }, () => {});
UtilData.createsSubscriptions(onSuccess);
});
};
optionMapper (it: any) {
return it._empty_ ? null : { ...it, object: it };
};
};
export default new Util();

128
extension/popup.tsx Normal file
View file

@ -0,0 +1,128 @@
import * as React from 'react';
import * as hs from 'history';
import $ from 'jquery';
import { Router, Route, Switch } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import { Provider } from 'mobx-react';
import { configure } from 'mobx';
import { ListMenu } from 'Component';
import { dispatcher, C, UtilCommon, UtilRouter } from 'Lib';
import { commonStore, authStore, blockStore, detailStore, dbStore, menuStore, popupStore, extensionStore } from 'Store';
import Extension from 'json/extension.json';
import Index from './popup/index';
import Challenge from './popup/challenge';
import Create from './popup/create';
import Success from './popup/success';
import './scss/popup.scss';
configure({ enforceActions: 'never' });
const Routes = [
{ path: '/' },
{ path: '/:page' },
];
const Components = {
index: Index,
challenge: Challenge,
create: Create,
success: Success,
};
const memoryHistory = hs.createMemoryHistory;
const history = memoryHistory();
const rootStore = {
commonStore,
authStore,
blockStore,
detailStore,
dbStore,
menuStore,
popupStore,
extensionStore,
};
declare global {
interface Window {
Electron: any;
$: any;
Anytype: any;
isWebVersion: boolean;
AnytypeGlobalConfig: any;
}
};
window.$ = $;
window.Anytype = {
Store: rootStore,
Lib: {
C,
UtilCommon,
dispatcher,
Storage,
},
};
class RoutePage extends React.Component<RouteComponentProps> {
render () {
const { match } = this.props;
const params = match.params as any;
const page = params.page || 'index';
const Component = Components[page];
return (
<React.Fragment>
<ListMenu key="listMenu" {...this.props} />
{Component ? <Component /> : null}
</React.Fragment>
);
};
};
class Popup extends React.Component {
node: any = null;
constructor (props: any) {
super(props);
};
render () {
return (
<Router history={history}>
<Provider {...rootStore}>
<div ref={node => this.node = node}>
<Switch>
{Routes.map((item: any, i: number) => (
<Route path={item.path} exact={true} key={i} component={RoutePage} />
))}
</Switch>
</div>
</Provider>
</Router>
);
};
componentDidMount () {
UtilRouter.init(history);
/* @ts-ignore */
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log('[Popup]', msg, sender);
if (sender.id != Extension.clipper.id) {
return false;
};
//sendResponse({ type: msg.type, ref: 'popup' });
return true;
});
};
};
export default Popup;

View file

@ -0,0 +1,57 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Button, Input, Error } from 'Component';
import { I, C, Storage, UtilRouter } from 'Lib';
import { extensionStore } from 'Store';
import Util from '../lib/util';
interface State {
error: string;
};
const Challenge = observer(class Challenge extends React.Component<I.PageComponent, State> {
ref: any = null;
state = {
error: '',
};
constructor (props: I.PageComponent) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
};
render () {
const { error } = this.state;
return (
<form className="page pageChallenge" onSubmit={this.onSubmit}>
<Input ref={ref => this.ref = ref} placeholder="Challenge" />
<div className="buttons">
<Button type="input" color="pink" className="c32" text="Authorize" />
</div>
<Error text={error} />
</form>
);
};
onSubmit (e: any) {
e.preventDefault();
C.AccountLocalLinkSolveChallenge(extensionStore.challengeId, this.ref?.getValue().trim(), (message: any) => {
if (message.error.code) {
this.setState({ error: message.error.description });
return;
};
Storage.set('appKey', message.appKey);
Util.authorize(message.appKey, () => UtilRouter.go('/create', {}));
});
};
});
export default Challenge;

453
extension/popup/create.tsx Normal file
View file

@ -0,0 +1,453 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import arrayMove from 'array-move';
import { getRange, setRange } from 'selection-ranges';
import { Label, Input, Button, Select, Loader, Error, DragBox, Tag, Textarea } from 'Component';
import { I, C, UtilCommon, UtilData, Relation, keyboard, UtilObject, UtilRouter } from 'Lib';
import { dbStore, detailStore, commonStore, menuStore, extensionStore } from 'Store';
import Constant from 'json/constant.json';
import Util from '../lib/util';
interface State {
error: string;
isLoading: boolean;
};
const MAX_LENGTH = 320;
const Create = observer(class Create extends React.Component<I.PageComponent, State> {
details: any = {
type: '',
tag: [],
};
node: any = null;
refName: any = null;
refSpace: any = null;
refType: any = null;
refComment: any = null;
isCreating = false;
url = '';
state = {
isLoading: false,
error: '',
};
constructor (props: I.PageComponent) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onSpaceChange = this.onSpaceChange.bind(this);
this.onTypeChange = this.onTypeChange.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onInput = this.onInput.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.focus = this.focus.bind(this);
};
render () {
const { isLoading, error } = this.state;
const { space } = commonStore;
const tags = this.getTagsValue();
return (
<div
ref={ref => this.node = ref}
className="page pageCreate"
>
{isLoading ? <Loader type="loader" /> : ''}
<form onSubmit={this.onSubmit}>
<div className="rows">
<div className="row">
<Label text="Title" />
<Input ref={ref => this.refName = ref} />
</div>
<div className="row">
<Label text="Space" />
<Select
id="select-space"
ref={ref => this.refSpace = ref}
value=""
options={[]}
onChange={this.onSpaceChange}
menuParam={{
horizontal: I.MenuDirection.Center,
data: { maxHeight: 180 }
}}
/>
</div>
<div className="row">
<Label text="Save as" />
<Select
id="select-type"
ref={ref => this.refType = ref}
readonly={!space}
value=""
options={[]}
onChange={this.onTypeChange}
menuParam={{
horizontal: I.MenuDirection.Center,
data: { maxHeight: 180 }
}}
/>
</div>
<div className="row">
<Label text="Tag" />
<div id="select-tag" className="box cell isEditing c-select" onClick={this.focus}>
<div className="value cellContent c-select">
<span id="list">
<DragBox onDragEnd={this.onDragEnd}>
{tags.map((item: any, i: number) => (
<span
key={i}
id={`item-${item.id}`}
className="itemWrap isDraggable"
draggable={true}
{...UtilCommon.dataProps({ id: item.id, index: i })}
>
<Tag
key={item.id}
text={item.name}
color={item.color}
canEdit={true}
className={Relation.selectClassName(I.RelationType.MultiSelect)}
onRemove={() => this.onValueRemove(item.id)}
/>
</span>
))}
</DragBox>
</span>
<span className="entryWrap">
<span
id="entry"
contentEditable={true}
suppressContentEditableWarning={true}
onFocus={this.onFocus}
onBlur={this.onBlur}
onInput={this.onInput}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
>
{'\n'}
</span>
<div id="placeholder" className="placeholder">Type...</div>
</span>
</div>
</div>
</div>
</div>
<div className="buttons">
<Button color="pink" className="c32" text="Save" type="input" subType="submit" onClick={this.onSubmit} />
</div>
<Error text={error} />
</form>
</div>
);
};
componentDidMount(): void {
this.initSpace();
this.initName();
this.initType();
};
componentDidUpdate(): void {
this.initType();
};
initSpace () {
const spaces = this.getSpaces();
if (!this.refSpace || !spaces.length) {
return;
};
const space = commonStore.space || spaces[0].targetSpaceId;
this.refSpace.setOptions(spaces);
this.refSpace.setValue(space);
this.onSpaceChange(space);
};
initType () {
const options = this.getTypes().map(it => ({ ...it, id: it.uniqueKey }));
if (!this.refType || !options.length) {
return;
};
this.details.type = this.details.type || options[0].id;
this.refType.setOptions(options);
this.refType.setValue(this.details.type);
};
initName () {
if (!this.refName) {
return;
};
Util.getCurrentTab(tab => {
if (!tab) {
return;
};
this.refName.setValue(tab.title);
this.refName.focus();
this.url = tab.url;
});
};
getObjects (subId: string) {
return dbStore.getRecords(subId, '').map(id => detailStore.get(subId, id));
};
getSpaces () {
return dbStore.getSpaces().map(it => ({ ...it, id: it.targetSpaceId, object: it })).filter(it => it)
};
getTypes () {
const layouts = UtilObject.getPageLayouts();
return this.getObjects(Constant.subId.type).
map(Util.optionMapper).
filter(this.filter).
filter(it => layouts.includes(it.recommendedLayout) && (it.spaceId == commonStore.space)).
sort(UtilData.sortByName);
};
filter (it: any) {
return it && !it.isHidden && !it.isArchived && !it.isDeleted;
};
onTypeChange (id: string): void {
this.details.type = id;
this.forceUpdate();
};
onSpaceChange (id: string): void {
commonStore.spaceSet(id);
UtilData.createsSubscriptions(() => this.forceUpdate());
};
getTagsValue () {
return dbStore.getRecords(Constant.subId.option, '').
filter(id => this.details.tag.includes(id)).
map(id => detailStore.get(Constant.subId.option, id)).
filter(it => it && !it._empty_);
};
clear () {
const node = $(this.node);
node.find('#entry').text(' ');
this.focus();
};
focus () {
const node = $(this.node);
const entry = node.find('#entry');
if (entry.length) {
window.setTimeout(() => {
entry.focus();
setRange(entry.get(0), { start: 0, end: 0 });
this.scrollToBottom();
});
};
};
onValueRemove (id: string) {
this.setValue(this.details.tag.filter(it => it != id));
};
onDragEnd (oldIndex: number, newIndex: number) {
this.setValue(arrayMove(this.details.tag, oldIndex, newIndex));
};
onKeyDown (e: any) {
const node = $(this.node);
const entry = node.find('#entry');
keyboard.shortcut('backspace', e, (pressed: string) => {
e.stopPropagation();
const range = getRange(entry.get(0));
if (range.start || range.end) {
return;
};
e.preventDefault();
const value = this.getValue();
value.existing.pop();
this.setValue(value.existing);
});
this.placeholderCheck();
this.scrollToBottom();
};
onKeyPress (e: any) {
const node = $(this.node);
const entry = node.find('#entry');
if (entry.length && (entry.text().length >= MAX_LENGTH)) {
e.preventDefault();
};
};
onKeyUp (e: any) {
menuStore.updateData('dataviewOptionList', { filter: this.getValue().new });
this.placeholderCheck();
this.resize();
this.scrollToBottom();
};
onInput () {
this.placeholderCheck();
};
onFocus () {
const relation = dbStore.getRelationByKey('tag');
const element = '#select-tag';
menuStore.open('dataviewOptionList', {
element,
horizontal: I.MenuDirection.Center,
commonFilter: true,
onOpen: () => {
window.setTimeout(() => { $(element).addClass('isFocused'); });
},
onClose: () => { $(element).removeClass('isFocused'); },
data: {
canAdd: true,
filter: '',
value: this.details.tag,
maxCount: relation.maxCount,
noFilter: true,
relation: observable.box(relation),
maxHeight: 120,
onChange: (value: string[]) => {
this.setValue(value);
}
}
});
};
onBlur () {
};
placeholderCheck () {
const node = $(this.node);
const value = this.getValue();
const list = node.find('#list');
const placeholder = node.find('#placeholder');
if (value.existing.length) {
list.show();
} else {
list.hide();
};
if (value.new || value.existing.length) {
placeholder.hide();
} else {
placeholder.show();
};
};
getValue () {
const node = $(this.node);
const list = node.find('#list');
const items = list.find('.itemWrap');
const entry = node.find('#entry');
const existing: any[] = [];
items.each((i: number, item: any) => {
item = $(item);
existing.push(item.data('id'));
});
return {
existing,
new: (entry.length ? String(entry.text() || '').trim() : ''),
};
};
setValue (value: string[]) {
const relation = dbStore.getRelationByKey('tag');
value = UtilCommon.arrayUnique(value);
const length = value.length;
if (relation.maxCount && (length > relation.maxCount)) {
value = value.slice(length - relation.maxCount, length);
};
this.details.tag = value;
this.clear();
this.forceUpdate();
};
onSubmit (e: any) {
e.preventDefault();
if (this.isCreating) {
return;
};
this.isCreating = true;
this.setState({ isLoading: true, error: '' });
const details = Object.assign({ name: this.refName?.getValue(), origin: I.ObjectOrigin.Webclipper }, this.details);
const type = details.type;
delete(details.type);
C.ObjectCreateFromUrl(details, commonStore.space, type, this.url, (message: any) => {
this.setState({ isLoading: false });
if (message.error.code) {
this.setState({ error: message.error.description });
} else {
extensionStore.createdObject = message.details;
UtilRouter.go('/success', {});
};
this.isCreating = false;
});
};
scrollToBottom () {
const node = $(this.node);
const content: any = node.find('.cellContent');
content.scrollTop(content.get(0).scrollHeight + parseInt(content.css('paddingBottom')));
};
resize () {
$(window).trigger('resize.menuDataviewOptionList');
};
});
export default Create;

126
extension/popup/index.tsx Normal file
View file

@ -0,0 +1,126 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Label, Button, Error } from 'Component';
import { I, C, UtilRouter, Storage } from 'Lib';
import { extensionStore } from 'Store';
import Url from 'json/url.json';
import Util from '../lib/util';
interface State {
error: string;
};
const Index = observer(class Index extends React.Component<I.PageComponent, State> {
state = {
error: '',
};
interval: any = 0;
constructor (props: I.PageComponent) {
super(props);
this.onOpen = this.onOpen.bind(this);
this.onDownload = this.onDownload.bind(this);
};
render () {
const { error } = this.state;
return (
<div className="page pageIndex">
<Label text="To save in Anytype you need to Pair with the app" />
<div className="buttons">
<Button color="pink" className="c32" text="Pair with app" onClick={this.onOpen} />
<Button color="blank" className="c32" text="Download app" onClick={this.onDownload} />
</div>
<Error text={error} />
</div>
);
};
componentDidMount(): void {
this.checkPorts();
};
checkPorts (onError?: () => void): void {
Util.sendMessage({ type: 'getPorts' }, response => {
Util.sendMessage({ type: 'checkPorts' }, response => {
console.log('[Popup] checkPorts', response);
if (!response.ports || !response.ports.length) {
this.setState({ error: 'Automatic pairing failed, please open the app' });
if (onError) {
onError();
};
return;
};
Util.init(response.ports[1], response.ports[2]);
this.login();
});
});
};
login () {
const appKey = Storage.get('appKey');
if (appKey) {
Util.authorize(appKey, () => UtilRouter.go('/create', {}), () => {
Storage.delete('appKey');
this.login();
});
} else {
/* @ts-ignore */
const manifest = chrome.runtime.getManifest();
C.AccountLocalLinkNewChallenge(manifest.name, (message: any) => {
if (message.error.code) {
this.setState({ error: message.error.description });
return;
};
extensionStore.challengeId = message.challengeId;
UtilRouter.go('/challenge', {});
});
};
};
onOpen () {
const { serverPort, gatewayPort } = extensionStore;
if (serverPort && gatewayPort) {
this.login();
return;
};
let cnt = 0;
Util.sendMessage({ type: 'launchApp' }, response => {
this.interval = setInterval(() => {
this.checkPorts(() => {
cnt++;
if (cnt >= 30) {
this.setState({ error: 'App open failed' });
clearInterval(this.interval);
console.log('App open try', cnt);
};
});
}, 1000);
});
};
onDownload () {
window.open(Url.download);
};
});
export default Index;

View file

@ -0,0 +1,47 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Button } from 'Component';
import { I, UtilCommon, UtilObject } from 'Lib';
import { extensionStore } from 'Store';
import Url from 'json/url.json';
interface State {
error: string;
};
const Success = observer(class Success extends React.Component<I.PageComponent, State> {
constructor (props: I.PageComponent) {
super(props);
this.onOpen = this.onOpen.bind(this);
};
render () {
const object = extensionStore.createdObject;
if (!object) {
return null;
};
const name = object.name || UtilObject.defaultName('Page');
return (
<div className="page pageSuccess">
<div className="label bold">{UtilCommon.sprintf('"%s" is saved!', UtilCommon.shorten(name, 64))}</div>
<div className="label">{object.description}</div>
<div className="buttons">
<Button color="blank" className="c32" text="Open in app" onClick={this.onOpen} />
</div>
</div>
);
};
onOpen () {
window.open(Url.protocol + UtilObject.route(extensionStore.createdObject));
};
});
export default Success;

View file

@ -0,0 +1,75 @@
html.anytypeWebclipper-iframe,
html.anytypeWebclipper-popup {
@import "~scss/font.scss";
@import "~scss/_vars";
/* Text */
--color-text-primary: #252525;
--color-text-secondary: #949494;
--color-text-tertiary: #bfbfbf;
--color-text-inversion: #fff;
/* Shape */
--color-shape-primary: #e3e3e3;
--color-shape-secondary: #ebebeb;
--color-shape-tertiary: #f2f2f2;
--color-shape-highlight-medium: rgba(79, 79, 79, 0.08);
--color-shape-highlight-light: rgba(79, 79, 79, 0.04);
/* Control */
--color-control-accent: #252525;
--color-control-active: #b6b6b6;
--color-control-inactive: #dcdcdc;
--color-control-bg: #fff;
/* Background */
--color-bg-primary: #fff;
--color-bg-loader: rgba(255,255,255,0.7);
/* System */
--color-system-accent-100: #ffb522;
--color-system-accent-50: #ffd15b;
--color-system-accent-25: #ffee94;
--color-system-selection: rgba(24, 163, 241, 0.15);
--color-system-drop-zone: rgba(255, 187, 44, 0.25);
/* Color */
--color-yellow: #ecd91b;
--color-orange: #ffb522;
--color-red: #f55522;
--color-pink: #e51ca0;
--color-purple: #ab50cc;
--color-blue: #3e58eb;
--color-ice: #2aa7ee;
--color-teal: #0fc8ba;
--color-lime: #5dd400;
--color-green: #57c600;
@import "~scss/common.scss";
@import "~scss/form/common.scss";
@import "~scss/component/common.scss";
@import "~scss/menu/common.scss";
@import "~scss/block/common";
* { box-sizing: border-box; border: 0px; margin: 0px; padding: 0px; }
body { font-family: 'Inter'; @include text-common; }
.loaderWrapper { position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-bg-loader); z-index: 10; }
.menus {
.menu.vertical {
.item { cursor: pointer; }
}
}
.button, .select, .cellContent { cursor: pointer; }
.tagItem { @include text-small; }
}

View file

@ -0,0 +1,22 @@
#anytypeWebclipper-iframe {
@import "~scss/_vars";
.page.pageCreate { display: flex; flex-direction: column; }
.page.pageCreate {
.head { display: flex; flex-direction: row; align-items: center; padding: 8px; }
.head {
.side { display: flex; flex-direction: row; align-items: center; width: 50%; gap: 0px 14px; }
.side.right { justify-content: flex-end; }
.button.simple { @include text-common; font-weight: 500; cursor: pointer; height: 32px; line-height: 32px; }
.select { cursor: pointer; height: 32px; line-height: 32px; border: 0px; padding: 8px 20px 8px 8px; }
.select {
.item {
.name { line-height: 16px; }
}
}
}
.blocks { padding: 20px 48px 48px 0px; height: 100%; overflow: auto; }
}
}

50
extension/scss/popup.scss Normal file
View file

@ -0,0 +1,50 @@
html.anytypeWebclipper-popup { width: 268px; }
#anytypeWebclipper-popup {
@import "~scss/_vars";
.menus {
.menu.vertical { width: calc(100% - 32px); left: 16px; }
.menu.vertical {
.wrap, .items, .scrollArea, .ReactVirtualized__List { border-radius: inherit; }
}
}
.input, .select, .textarea { @include text-small; border: 1px solid var(--color-shape-secondary); width: 100%; border-radius: 1px; }
.input, .select { height: 32px; padding: 0px 10px; }
.select { display: flex; align-items: center; }
.textarea { padding: 10px; resize: none; height: 68px; display: block; }
.buttons { display: flex; flex-direction: column; justify-content: center; gap: 8px 0px; margin: 16px 0px 0px 0px; }
.loaderWrapper { position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-bg-loader); z-index: 10; }
.error { @include text-small; margin: 1em 0px 0px 0px; }
.isFocused { border-color: var(--color-ice) !important; box-shadow: 0px 0px 0px 1px var(--color-ice); }
.page.pageIndex, .page.pageChallenge { padding: 50px 16px; text-align: center; }
.page.pageCreate { padding: 16px; }
.page.pageCreate {
.row { margin: 0px 0px 10px 0px; }
.row:last-child { margin: 0px; }
.label { @include text-small; color: var(--color-text-secondary); margin: 0px 0px 4px 0px; }
.box { border: 1px solid var(--color-shape-secondary); border-radius: 1px; min-height: 32px; }
.box {
.value { padding: 6px 10px 0px 10px; }
.value {
.itemWrap { margin: 0px 6px 6px 0px; }
}
.entryWrap { position: relative; line-height: 18px; display: inline; vertical-align: top; }
.cellContent { height: auto !important; position: relative; box-shadow: 0px 0px; }
}
}
.page.pageSuccess { padding: 16px; text-align: center; }
.page.pageSuccess {
.label { @include text-small; }
.label.bold { @include text-common; font-weight: 600; }
}
}

378
go/nativeMessagingHost.go Normal file
View file

@ -0,0 +1,378 @@
/*
- This is the native messaging host for the AnyType browser extension.
- It enables the web extension to find the open ports of the AnyType application and to start it if it is not running.
- It is installed by the Electron script found in electron/js/lib/installNativeMessagingHost.js
- for more docs, checkout the webclipper repository: https://github.com/anytypeio/webclipper
*/
package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"unsafe"
)
// UTILITY FUNCTIONS
// splits stdout into an array of lines, removing empty lines
func splitStdOutLines(stdout string) []string {
lines := strings.Split(stdout, "\n")
filteredLines := make([]string, 0)
for _, line := range lines {
if len(line) > 0 {
filteredLines = append(filteredLines, line)
}
}
return filteredLines
}
// splits stdout into an array of tokens, replacing tabs with spaces
func splitStdOutTokens(line string) []string {
return strings.Fields(strings.Replace(line, "\t", " ", -1))
}
// executes a command and returns the stdout as string
func execCommand(command string) (string, error) {
stdout, err := exec.Command("bash", "-c", command).Output()
return string(stdout), err
}
// checks if a string is contained in an array of strings
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// CORE LOGIC
// Windows: returns a list of open ports for all instances of anytypeHelper.exe found using cli utilities tasklist, netstat and findstr
func getOpenPortsWindows() (map[string][]string, error) {
appName := "anytypeHelper.exe"
stdout, err := execCommand(`tasklist | findstr "` + appName + `"`)
if err != nil {
return nil, err
}
lines := splitStdOutLines(stdout)
pids := map[string]bool{}
for _, line := range lines {
tokens := splitStdOutTokens(line)
pids[tokens[1]] = true
}
if len(pids) == 0 {
return nil, errors.New("application not running")
}
result := map[string][]string{}
for pid := range pids {
stdout, err := execCommand(`netstat -ano | findstr ${pid} | findstr LISTENING`)
if err != nil {
return nil, err
}
lines := splitStdOutLines(stdout)
ports := map[string]bool{}
for _, line := range lines {
tokens := splitStdOutTokens(line)
port := strings.Split(tokens[1], ":")[1]
ports[port] = true
}
portsSlice := []string{}
for port := range ports {
portsSlice = append(portsSlice, port)
}
result[pid] = portsSlice
}
return result, nil
}
// MacOS and Linux: returns a list of all open ports for all instances of anytype found using cli utilities lsof and grep
func getOpenPortsUnix() (map[string][]string, error) {
// execute the command
appName := "anytype"
stdout, err := execCommand(`lsof -i -P -n | grep LISTEN | grep "` + appName + `"`)
Trace.Print(`lsof -i -P -n | grep LISTEN | grep "` + appName + `"`)
if err != nil {
Trace.Print(err)
return nil, err
}
// initialize the result map
result := make(map[string][]string)
// split the output into lines
lines := splitStdOutLines(stdout)
for _, line := range lines {
// normalize whitespace and split into tokens
tokens := splitStdOutTokens(line)
pid := tokens[1]
port := strings.Split(tokens[8], ":")[1]
// add the port to the result map
if _, ok := result[pid]; !ok {
result[pid] = []string{}
}
if !contains(result[pid], port) {
result[pid] = append(result[pid], port)
}
}
if len(result) == 0 {
return nil, errors.New("application not running")
}
return result, nil
}
// Windows, MacOS and Linux: returns a list of all open ports for all instances of anytype found using cli utilities
func getOpenPorts() (map[string][]string, error) {
// Get Platform
platform := runtime.GOOS
Trace.Print("Getting Open Ports on Platform: " + platform)
// Platform specific functions
if platform == "windows" {
return getOpenPortsWindows()
} else if platform == "darwin" {
return getOpenPortsUnix()
} else if platform == "linux" {
return getOpenPortsUnix()
} else {
return nil, errors.New("unsupported platform")
}
}
// Windows, MacOS and Linux: Starts AnyType as a detached process and returns the PID
func startApplication() (int, error) {
platform := runtime.GOOS
executablePath, err := os.Executable()
if err != nil {
return 0, err
}
// /Resources/app.asar.unpacked/dist/executable
appPath := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(executablePath))))
if platform == "windows" {
appPath = filepath.Join(appPath, "Anytype.exe")
} else if platform == "darwin" {
appPath = filepath.Join(appPath, "MacOS", "Anytype")
} else if platform == "linux" {
appPath = filepath.Join(appPath, "anytype")
} else {
return 0, errors.New("unsupported platform")
}
Trace.Print("Starting Application on Platform: " + platform + " with Path: " + appPath)
sub := exec.Command(appPath)
err = sub.Start()
sub.Process.Release()
if err != nil {
return 0, err
}
return sub.Process.Pid, nil
}
// MESSAGING LOGIC
// constants for Logger
var (
// Trace logs general information messages.
Trace *log.Logger
// Error logs error messages.
Error *log.Logger
)
// nativeEndian used to detect native byte order
var nativeEndian binary.ByteOrder
// bufferSize used to set size of IO buffer - adjust to accommodate message payloads
var bufferSize = 8192
// IncomingMessage represents a message sent to the native host.
type IncomingMessage struct {
Type string `json:"type"`
}
// OutgoingMessage respresents a response to an incoming message query.
type OutgoingMessage struct {
Type string `json:"type"`
Response interface{} `json:"response"`
Error interface{} `json:"error"`
}
// Init initializes logger and determines native byte order.
func Init(traceHandle io.Writer, errorHandle io.Writer) {
Trace = log.New(traceHandle, "TRACE: ", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(errorHandle, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// determine native byte order so that we can read message size correctly
var one int16 = 1
b := (*byte)(unsafe.Pointer(&one))
if *b == 0 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}
func main() {
file, err := os.OpenFile("nmh.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
Init(os.Stdout, os.Stderr)
Error.Printf("Unable to create and/or open log file. Will log to Stdout and Stderr. Error: %v", err)
} else {
Init(file, file)
// ensure we close the log file when we're done
defer file.Close()
}
Trace.Printf("Chrome native messaging host started. Native byte order: %v.", nativeEndian)
read()
Trace.Print("Chrome native messaging host exited.")
}
// read Creates a new buffered I/O reader and reads messages from Stdin.
func read() {
v := bufio.NewReader(os.Stdin)
// adjust buffer size to accommodate your json payload size limits; default is 4096
s := bufio.NewReaderSize(v, bufferSize)
Trace.Printf("IO buffer reader created with buffer size of %v.", s.Size())
lengthBytes := make([]byte, 4)
lengthNum := int(0)
// we're going to indefinitely read the first 4 bytes in buffer, which gives us the message length.
// if stdIn is closed we'll exit the loop and shut down host
for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) {
// convert message length bytes to integer value
lengthNum = readMessageLength(lengthBytes)
Trace.Printf("Message size in bytes: %v", lengthNum)
// If message length exceeds size of buffer, the message will be truncated.
// This will likely cause an error when we attempt to unmarshal message to JSON.
if lengthNum > bufferSize {
Error.Printf("Message size of %d exceeds buffer size of %d. Message will be truncated and is unlikely to unmarshal to JSON.", lengthNum, bufferSize)
}
// read the content of the message from buffer
content := make([]byte, lengthNum)
_, err := s.Read(content)
if err != nil && err != io.EOF {
Error.Fatal(err)
}
// message has been read, now parse and process
parseMessage(content)
}
Trace.Print("Stdin closed.")
}
// readMessageLength reads and returns the message length value in native byte order.
func readMessageLength(msg []byte) int {
var length uint32
buf := bytes.NewBuffer(msg)
err := binary.Read(buf, nativeEndian, &length)
if err != nil {
Error.Printf("Unable to read bytes representing message length: %v", err)
}
return int(length)
}
// parseMessage parses incoming message
func parseMessage(msg []byte) {
iMsg := decodeMessage(msg)
Trace.Printf("Message received: %s", msg)
// start building outgoing json message
oMsg := OutgoingMessage{
Type: iMsg.Type,
}
switch iMsg.Type {
case "getPorts":
// Get open ports
openPorts, err := getOpenPorts()
if err != nil {
oMsg.Error = err.Error()
} else {
oMsg.Response = openPorts
}
case "launchApp":
// Start application
pid, err := startApplication()
if err != nil {
oMsg.Error = err.Error()
} else {
oMsg.Response = pid
}
}
send(oMsg)
}
// decodeMessage unmarshals incoming json request and returns query value.
func decodeMessage(msg []byte) IncomingMessage {
var iMsg IncomingMessage
err := json.Unmarshal(msg, &iMsg)
if err != nil {
Error.Printf("Unable to unmarshal json to struct: %v", err)
}
return iMsg
}
// send sends an OutgoingMessage to os.Stdout.
func send(msg OutgoingMessage) {
byteMsg := dataToBytes(msg)
writeMessageLength(byteMsg)
var msgBuf bytes.Buffer
_, err := msgBuf.Write(byteMsg)
if err != nil {
Error.Printf("Unable to write message length to message buffer: %v", err)
}
_, err = msgBuf.WriteTo(os.Stdout)
if err != nil {
Error.Printf("Unable to write message buffer to Stdout: %v", err)
}
}
// dataToBytes marshals OutgoingMessage struct to slice of bytes
func dataToBytes(msg OutgoingMessage) []byte {
byteMsg, err := json.Marshal(msg)
if err != nil {
Error.Printf("Unable to marshal OutgoingMessage struct to slice of bytes: %v", err)
}
return byteMsg
}
// writeMessageLength determines length of message and writes it to os.Stdout.
func writeMessageLength(msg []byte) {
err := binary.Write(os.Stdout, nativeEndian, uint32(len(msg)))
if err != nil {
Error.Printf("Unable to write message length to Stdout: %v", err)
}
}

14
package-lock.json generated
View file

@ -83,7 +83,7 @@
"@typescript-eslint/parser": "^6.18.1",
"cross-env": "^7.0.2",
"css-loader": "^3.6.0",
"electron": "^28.1.3",
"electron": "^28.2.0",
"electron-builder": "^24.6.3",
"eslint": "^8.29.0",
"eslint-plugin-react": "^7.31.11",
@ -4963,9 +4963,9 @@
}
},
"node_modules/electron": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.1.3.tgz",
"integrity": "sha512-NSFyTo6SndTPXzU18XRePv4LnjmuM9rF5GMKta1/kPmi02ISoSRonnD7wUlWXD2x53XyJ6d/TbSVesMW6sXkEQ==",
"version": "28.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.2.0.tgz",
"integrity": "sha512-22SylXQQ9IHtwLw4D+Z4Si7OUpeDtpHfJVTjy3yv53iLg5zJKKPOCWT4ZwgYGHQZ0eldyBrYBHF/P9FPd2CcVQ==",
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
@ -15614,7 +15614,6 @@
},
"node_modules/open-color/node_modules/npm/node_modules/lodash._baseindexof": {
"version": "3.1.0",
"extraneous": true,
"inBundle": true,
"license": "MIT"
},
@ -15629,19 +15628,16 @@
},
"node_modules/open-color/node_modules/npm/node_modules/lodash._bindcallback": {
"version": "3.0.1",
"extraneous": true,
"inBundle": true,
"license": "MIT"
},
"node_modules/open-color/node_modules/npm/node_modules/lodash._cacheindexof": {
"version": "3.0.2",
"extraneous": true,
"inBundle": true,
"license": "MIT"
},
"node_modules/open-color/node_modules/npm/node_modules/lodash._createcache": {
"version": "3.1.2",
"extraneous": true,
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -15655,7 +15651,6 @@
},
"node_modules/open-color/node_modules/npm/node_modules/lodash._getnative": {
"version": "3.9.1",
"extraneous": true,
"inBundle": true,
"license": "MIT"
},
@ -15671,7 +15666,6 @@
},
"node_modules/open-color/node_modules/npm/node_modules/lodash.restparam": {
"version": "3.6.1",
"extraneous": true,
"inBundle": true,
"license": "MIT"
},

View file

@ -1,6 +1,6 @@
[
"electron.js",
"electron/js/*",
"electron/js/**/*",
"electron/json/*",
"electron/env.json",
"electron/img/*",
@ -12,12 +12,15 @@
"dist/main.js.map",
"dist/run.js",
"dist/embed/**/*",
"dist/challenge/**/*",
"dist/lib/**/*",
"dist/img/**/*",
"dist/css/**/*",
"dist/js/**/*",
"dist/anytypeHelper.exe",
"dist/anytypeHelper",
"dist/nativeMessagingHost.exe",
"dist/nativeMessagingHost",
"dist/*.node",
"dist/font/**/*",
"dist/workers/**/*",

View file

@ -15,7 +15,10 @@
"start:dev": "npm-run-all --parallel start:watch start:electron-wait-webpack",
"start:dev-win": "npm-run-all --parallel start:watch start:electron-wait-webpack-win",
"build": "webpack --mode=production --node-env=production --config webpack.config.js",
"build:dev": "webpack --mode=development --node-env=development --config webpack.config.js",
"build:deps": "webpack --config webpack.node.config.js --stats detailed | grep 'node_modules' | sed -E 's/.*(node_modules[\\/][^\\\\/[:space:]]{1,})[\\\\/].*/\\1/' | uniq | node save-node-deps.js",
"build:nmh": "go build -o dist/nativeMessagingHost ./go/nativeMessagingHost.go",
"build:nmh-win": "go build -o dist/nativeMessagingHost.exe ./gonativeMessagingHost.go",
"dist:mac": "npm run build:deps && webpack --progress --mode=production --node-env=production && DATE=`date '+%Y-%m-%d_%H_%M'` GIT_COMMIT=`git rev-parse --short HEAD` electron-builder --macos --arm64 --x64",
"dist:macarm": "npm run build:deps && webpack --mode=production --node-env=production && DATE=`date '+%Y-%m-%d_%H_%M'` GIT_COMMIT=`git rev-parse --short HEAD` electron-builder --macos --arm64",
"dist:macamd": "npm run build:deps && webpack --mode=production --node-env=production && DATE=`date '+%Y-%m-%d_%H_%M'` GIT_COMMIT=`git rev-parse --short HEAD` electron-builder --macos --x64",
@ -59,7 +62,7 @@
"@typescript-eslint/parser": "^6.18.1",
"cross-env": "^7.0.2",
"css-loader": "^3.6.0",
"electron": "^28.1.3",
"electron": "^28.2.0",
"electron-builder": "^24.6.3",
"eslint": "^8.29.0",
"eslint-plugin-react": "^7.31.11",
@ -169,10 +172,13 @@
"dist/lib",
"dist/anytypeHelper",
"dist/anytypeHelper.exe",
"dist/nativeMessagingHost.exe",
"dist/nativeMessagingHost",
"dist/font/**/*",
"dist/workers/**/*",
"dist/*.node",
"dist/embed/**/*",
"dist/challenge/**/*",
"dist/img/**/*",
"dist/css/**/*",
"dist/js/**/*",
@ -192,7 +198,7 @@
"extraResources": [],
"files": [
"electron.js",
"electron/js/*",
"electron/js/**/*",
"electron/json/*",
"electron/env.json",
"electron/img/*",
@ -204,12 +210,15 @@
"dist/main.js.map",
"dist/run.js",
"dist/embed/**/*",
"dist/challenge/**/*",
"dist/lib/**/*",
"dist/img/**/*",
"dist/css/**/*",
"dist/js/**/*",
"dist/anytypeHelper.exe",
"dist/anytypeHelper",
"dist/nativeMessagingHost.exe",
"dist/nativeMessagingHost",
"dist/*.node",
"dist/font/**/*",
"dist/workers/**/*",
@ -572,4 +581,4 @@
"pre-commit": "npm run precommit && git add licenses.json"
}
}
}
}

View file

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 720 B

Before After
Before After

View file

@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.46967 4.46967C4.76256 4.17678 5.23744 4.17678 5.53033 4.46967L15.5303 14.4697C15.8232 14.7626 15.8232 15.2374 15.5303 15.5303C15.2374 15.8232 14.7626 15.8232 14.4697 15.5303L4.46967 5.53033C4.17678 5.23744 4.17678 4.76256 4.46967 4.46967Z" fill="#252525"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5303 4.46967C15.2374 4.17678 14.7626 4.17678 14.4697 4.46967L4.46967 14.4697C4.17678 14.7626 4.17678 15.2374 4.46967 15.5303C4.76256 15.8232 5.23744 15.8232 5.53033 15.5303L15.5303 5.53033C15.8232 5.23744 15.8232 4.76256 15.5303 4.46967Z" fill="#252525"/>
</svg>

Before

Width:  |  Height:  |  Size: 720 B

View file

@ -50,7 +50,7 @@
"notification": 200
},
"extension": {
"fileExtension": {
"image": [ "jpg", "jpeg", "png", "gif", "svg", "webp" ],
"video": [ "mp4", "m4v", "mov" ],
"cover": [ "jpg", "jpeg", "png" ],
@ -97,7 +97,7 @@
"sidebarRelationKeys": [
"id", "spaceId", "name", "description", "snippet", "layout", "type", "iconEmoji", "iconImage", "iconOption", "isReadonly", "isHidden", "isDeleted", "isArchived", "isFavorite", "done",
"relationFormat", "fileExt", "fileMimeType", "links", "restrictions", "source", "identityProfileLink"
"relationFormat", "fileExt", "fileMimeType", "links", "restrictions", "source", "identityProfileLink", "lastModifiedDate", "lastOpenedDate"
],
"relationRelationKeys": [

7
src/json/extension.json Normal file
View file

@ -0,0 +1,7 @@
{
"clipper": {
"id": "jkmhmgghdjjbafmkgjmplhemjjnkligf",
"name": "Anytype Webclipper",
"prefix": "anytypeWebclipper"
}
}

View file

@ -40,6 +40,9 @@
"commonToday": "Today",
"commonTomorrow": "Tomorrow",
"commonYesterday": "Yesterday",
"commonLastWeek": "Previous 7 days",
"commonLastMonth": "Previous 30 days",
"commonOlder": "Older",
"commonPhrase": "Recovery Phrase",
"commonCopy": "Copy",
"commonOpen": "Open",
@ -115,6 +118,7 @@
"commonSetName": "Set of %s",
"commonClear": "Clear",
"commonNewObject": "New Object",
"commonSelectObject": "Select object",
"commonCopyLink": "Copy link",
"commonSidebar": "Sidebar",
"commonLanguage": "Language",

View file

@ -14,5 +14,6 @@
"privacy": "https://anytype.io/app_privacy/",
"contact": "mailto:support@anytype.io?subject=Support%20request%2C%20account%20%25accountId%25&body=%0A%0ATechnical%20information%0A----------------------------------------------%0AOS%20version%3A%20%25os%25%0AApp%20version%3A%20%25version%25%0ABuild%20number%3A%20%25build%25%0ALibrary%20version%3A%20%25middleware%25%0AAccount%20ID%3A%20%25accountId%25%0AAnalytics%20ID%3A%20%25analyticsId%25%0ADevice%20ID%3A%20%25deviceId%25",
"extendStorage": "mailto:storage@anytype.io?subject=Get%20more%20storage%2C%20account%20%25accountId%25&body=Hi%2C%20Anytype%20team.%20I%20am%20reaching%20out%20to%20request%20an%20increase%20in%20my%20file%20storage%20capacity%20as%20I%20have%20run%20out%20of%20storage.%20My%20current%20limit%20is%20%25storageLimit%25.%20My%20account%20id%20is%20%25accountId%25.%20Cheers%2C%20%25spaceName%25",
"gallery": "https://gallery.any.coop"
"gallery": "https://gallery.any.coop",
"emojiPrefix": "https://anytype-static.fra1.cdn.digitaloceanspaces.com/emojies/"
}

38
src/scss/block/latex.scss Normal file
View file

@ -0,0 +1,38 @@
@import "~scss/_vars";
.blocks {
.block.blockLatex { padding: 6px 0px; }
.block.blockLatex {
.wrap { padding: 2px 0px; }
.wrap.isEditing { padding: 8px; box-shadow: 0px 0px 0px 1px $colorShapePrimary; border-radius: 6px; }
.wrap.isEditing {
.empty { padding-bottom: 18px; }
#input { display: block; }
.select { display: inline-block; }
}
.selectWrap { text-align: left; }
.selectWrap {
.select {
border: 0px; color: $colorControlActive; @include text-common; border-radius: 0px; padding: 0px 20px 0px 0px;
display: none; margin-bottom: 8px;
}
.select {
.name { overflow: visible; }
}
.select:hover, .select.isFocused { background: none; }
}
#value { font-size: 20px; line-height: 20px; width: 100%; }
#value:empty { display: none; }
#input {
background: $colorShapeTertiary; text-align: left; font-family: 'Inter'; padding: 8px; @include text-common;
-webkit-user-modify: read-write-plaintext-only; display: none; border-radius: 4px; margin-top: 8px;
}
.katex-display { margin: 0px; text-align: inherit; }
.katex { line-height: 1.5em; text-align: inherit; }
.katex > .katex-html { white-space: normal; }
.katex .base { margin-top: 2px; margin-bottom: 2px; }
}
}

View file

@ -192,4 +192,7 @@ search.active { background: orange !important; }
.icon.resize { display: none; }
.icon.download { display: none; }
}
}
@import "scss/debug.scss";
@import "scss/font.scss";

View file

@ -22,8 +22,11 @@
.fill { position: absolute; left: 0px; top: 0px; height: 100%; background: var(--color-control-accent); transition: width 0.2s linear; }
}
.icon.close { width: 20px; height: 20px; position: absolute; top: 14px; right: 14px; background-image: url('~img/icon/progress/close0.svg'); }
.icon.close:hover { background-image: url('~img/icon/progress/close1.svg'); }
.icon.close {
width: 24px; height: 24px; position: absolute; top: 14px; right: 14px; background-image: url('~img/icon/progress/close.svg');
cursor: default; background-size: 20px; border-radius: 4px;
}
.icon.close:hover { background-color: var(--color-shape-highlight-medium); }
}
.progress.hide { background: rgba(0,0,0,0); }

View file

@ -13,7 +13,10 @@
}
.button.orange { background: var(--color-system-accent-100); color: var(--color-bg-primary); }
.button.orange:not(.disabled):hover, .button.orange:not(.disabled).hover { background: var(--color-system-accent-100); }
.button.orange:not(.disabled):hover, .button.orange:not(.disabled).hover { background: #f09c0e; }
.button.pink { background: #ff6a7b; color: var(--color-text-inversion); }
.button.pink:not(.disabled):hover { background: #e5374b; }
.button.black { background: var(--color-control-accent); color: var(--color-bg-primary); }
.button.black:not(.disabled):hover, .button.black:not(.disabled).hover { background: #41403d; }
@ -34,6 +37,7 @@
.button.blank:not(.disabled).hover { background: var(--color-shape-highlight-medium); }
.button.c36 { @include text-common; height: 36px; border-radius: 6px; padding: 0px 12px; }
.button.c32{ @include text-small; height: 32px; border-radius: 6px; padding: 0px 10px; }
.button.c28 { @include text-common; height: 28px; border-radius: 6px; padding: 0px 10px; }
.button.c16 { @include text-9; height: 16px; border-radius: 4px; padding: 0px 4px; }
@ -44,9 +48,5 @@ input.button { line-height: 1; }
.arrow { display: inline-block; width: 8px; height: 8px; margin: 0px 0px 0px 4px; }
}
.button.simple {
height: auto; padding: 0px; color: var(--color-control-active); font-weight: bold; line-height: 1.43;
letter-spacing: 0.1px;
}
.button.simple:hover,
.button.simple.hover { color: var(--color-text-primary); }
.button.simple { height: auto; padding: 0px; color: var(--color-control-active); font-weight: bold; line-height: 1.43; letter-spacing: 0.1px; }
.button.simple:hover, .button.simple.hover { color: var(--color-text-primary); }

View file

@ -5,7 +5,7 @@
transition: $transitionAllCommon; position: relative; font-family: 'Inter';
white-space: nowrap; padding: 3px 20px 3px 6px; line-height: 20px !important;
}
.select:hover, .select.active { background: var(--color-shape-tertiary); }
.select:hover, .select.isFocused { background: var(--color-shape-tertiary); }
.select {
.icon { transition: none; }
@ -22,6 +22,8 @@
.caption { display: none; }
}
.item::before { display: none; }
.clickable { display: flex; }
}
.select.big { padding: 9px 26px 9px 12px; border-radius: 10px; }

View file

@ -125,7 +125,7 @@
.iconObject { margin-right: 6px; vertical-align: top; flex-shrink: 0; }
.clickable { display: flex; flex-grow: 1; width: 100%; }
.clickable { display: flex; flex-grow: 1; width: 100%; align-items: center; }
.select { height: 20px; padding-top: 0px; padding-bottom: 0px; }
.select {

View file

@ -18,7 +18,7 @@
}
.select { overflow: hidden; border: 0px; padding: 0px; display: block; }
.select:hover, .select.active { background: none; }
.select:hover, .select.isFocused { background: none; }
.select {
.icon.relation { display: none; }
.icon.arrow { display: none; }
@ -61,7 +61,7 @@
.menu.menuDataviewFilterValues { width: 288px; }
.menu.menuDataviewFilterValues {
.select { border: 0px; padding: 0px; display: block; width: 100%; }
.select:hover, .select.active { background: none; }
.select:hover, .select.isFocused { background: none; }
.select {
.icon.arrow { background-image: url('~img/arrow/filter.svg') !important; }
}

View file

@ -12,7 +12,7 @@
.iconObject { margin-right: 10px; }
.select { border: 0px; padding: 0px; display: block; }
.select:hover, .select.active { background: none; }
.select:hover, .select.isFocused { background: none; }
.select.grey { color: var(--color-control-active); }
.select {
.icon.relation { display: none; }

View file

@ -19,7 +19,7 @@
.name { @include text-overflow-nw; width: 100%; vertical-align: middle; }
.select { overflow: hidden; border: 0px; padding: 0px; display: block; }
.select:hover, .select.active { background: none; }
.select:hover, .select.isFocused { background: none; }
.select {
.icon.relation { display: none; }
.icon.arrow { display: none; }

View file

@ -1,19 +1,6 @@
@import "~scss/_vars";
.menus {
.menu.menuObjectTypeEdit {
.content { padding-bottom: 11px; }
.wrap { padding: 0px 16px 8px 16px; }
.input { border: 1px solid var(--color-shape-secondary); padding: 0px 8px; }
.buttons { padding: 0px 14px; margin-top: 12px; }
.button { width: 100%; height: 28px; line-height: 28px; }
.icon.arrow {
width: 20px; height: 20px; position: absolute; right: 14px; top: 50%; margin: -10px 0px 0px 0px;
background-image: url('~img/arrow/menu.svg');
}
}
.menu.menuTypeSuggest {
.filter { padding-top: 12px; }
.content { max-height: unset; overflow: hidden; padding: 0px; transition: none; }

View file

@ -75,7 +75,7 @@
.title { margin-bottom: 10px; }
.content { line-height: 20px; color: var(--color-text-secondary); }
.select { border: 0px; padding: 3px 20px 3px 4px; cursor: default; }
.select:hover, .select.active { background: var(--color-shape-highlight-light); }
.select:hover, .select.isFocused { background: var(--color-shape-highlight-light); }
.icon { width: 20px; height: 20px; margin-right: 6px; }
.name { display: inline-block; vertical-align: middle; }

View file

@ -168,7 +168,7 @@
.sections { display: flex; flex-direction: column; height: 100%; }
.sections {
.section { margin-bottom: 40px; display: flex; flex-direction: column; gap: 12px 0px; }
.section.top { margin-bottom: 16px; }
.section.top { margin-bottom: 16px; justify-content: flex-start; flex-direction: row; }
.section.bottom { height: 100%; flex-direction: row; align-items: end; margin-bottom: 0; }
.section {

View file

@ -58,6 +58,12 @@
}
> .dropTarget.targetTop.isOver::before { top: 0px; }
> .dropTarget.targetBot.isOver::before { bottom: 0px; }
.item.isSection {
.inner { display: flex; align-items: center; }
.label { @include text-small; color: var(--color-text-secondary); @include text-overflow-nw; }
}
.item.isSection::before { display: none !important; }
}
.widget.active {
@ -101,4 +107,4 @@
@import "./space.scss";
@import "./list.scss";
@import "./tree.scss";
@import "./tree.scss";

View file

@ -1,7 +1,6 @@
import * as React from 'react';
import * as hs from 'history';
import * as Sentry from '@sentry/browser';
import mermaid from 'mermaid';
import $ from 'jquery';
import raf from 'raf';
import { RouteComponentProps } from 'react-router';
@ -28,8 +27,6 @@ import 'react-pdf/dist/cjs/Page/AnnotationLayer.css';
import 'react-pdf/dist/cjs/Page/TextLayer.css';
import 'scss/common.scss';
import 'scss/debug.scss';
import 'scss/font.scss';
import 'scss/component/common.scss';
import 'scss/page/common.scss';
import 'scss/block/common.scss';
@ -64,6 +61,7 @@ declare global {
isWebVersion: boolean;
Config: any;
AnytypeGlobalConfig: any;
}
};
@ -88,7 +86,7 @@ const rootStore = {
window.$ = $;
if (!window.Electron.isPackaged) {
if (!UtilCommon.getElectron().isPackaged) {
window.Anytype = {
Store: rootStore,
Lib: {
@ -134,8 +132,8 @@ enableLogging({
*/
Sentry.init({
release: window.Electron.version.app,
environment: window.Electron.isPackaged ? 'production' : 'development',
release: UtilCommon.getElectron().version.app,
environment: UtilCommon.getElectron().isPackaged ? 'production' : 'development',
dsn: Constant.sentry,
maxBreadcrumbs: 0,
beforeSend: (e: any) => {
@ -206,7 +204,7 @@ class App extends React.Component<object, State> {
<div id="root-loader" className="loaderWrapper">
<div className="inner">
<div className="logo anim from" />
<div className="version anim from">{window.Electron.version.app}</div>
<div className="version anim from">{UtilCommon.getElectron().version.app}</div>
</div>
</div>
) : ''}
@ -238,14 +236,16 @@ class App extends React.Component<object, State> {
init () {
UtilRouter.init(history);
dispatcher.init(window.Electron.getGlobal('serverAddress'));
dispatcher.init(UtilCommon.getElectron().getGlobal('serverAddress'));
dispatcher.listenEvents();
keyboard.init();
this.registerIpcEvents();
Renderer.send('appOnLoad');
console.log('[Process] os version:', window.Electron.version.system, 'arch:', window.Electron.arch);
console.log('[App] version:', window.Electron.version.app, 'isPackaged', window.Electron.isPackaged);
console.log('[Process] os version:', UtilCommon.getElectron().version.system, 'arch:', UtilCommon.getElectron().arch);
console.log('[App] version:', UtilCommon.getElectron().version.app, 'isPackaged', UtilCommon.getElectron().isPackaged);
};
initStorage () {
@ -483,7 +483,7 @@ class App extends React.Component<object, State> {
popupStore.open('confirm', {
data: {
title: translate('popupConfirmUpdateDoneTitle'),
text: UtilCommon.sprintf(translate('popupConfirmUpdateDoneText'), window.Electron.version.app),
text: UtilCommon.sprintf(translate('popupConfirmUpdateDoneText'), UtilCommon.getElectron().version.app),
textConfirm: translate('popupConfirmUpdateDoneOk'),
canCancel: false,
},

View file

@ -217,7 +217,7 @@ const BlockCover = observer(class BlockCover extends React.Component<I.BlockComp
onIconUser () {
const { rootId } = this.props;
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
C.FileUpload(commonStore.space, '', paths[0], I.FileType.Image, (message: any) => {
if (!message.error.code) {
UtilObject.setIcon(rootId, '', message.hash);

View file

@ -981,7 +981,7 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props
analytics.event('InlineSetSetSource', { type: isNew ? 'newObject': 'externalObject' });
};
const menuParam = Object.assign({
menuStore.open('searchObject', Object.assign({
element: $(element),
className: 'single',
data: {
@ -994,9 +994,7 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props
addParam,
onSelect,
}
}, param || {});
menuStore.open('searchObject', menuParam);
}, param || {}));
};
onSourceTypeSelect (obj: any) {

View file

@ -263,7 +263,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
case I.RelationType.File: {
param = Object.assign(param, {
width: width,
width,
});
param.data = Object.assign(param.data, {
value: value || [],
@ -276,7 +276,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
case I.RelationType.Select:
case I.RelationType.MultiSelect: {
param = Object.assign(param, {
width: width,
width,
commonFilter: true,
});
param.data = Object.assign(param.data, {
@ -295,7 +295,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
case I.RelationType.Object: {
param = Object.assign(param, {
width: width,
width,
commonFilter: true,
});
param.data = Object.assign(param.data, {
@ -324,7 +324,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
element: cell,
horizontal: I.MenuDirection.Left,
offsetY: -height,
width: width,
width,
height: height,
});

View file

@ -262,8 +262,9 @@ const CellObject = observer(class CellObject extends React.Component<I.Cell, Sta
value = UtilCommon.arrayUnique(value);
if (maxCount && value.length > maxCount) {
value = value.slice(value.length - maxCount, value.length);
const length = value.length;
if (maxCount && (length > maxCount)) {
value = value.slice(length - maxCount, length);
};
if (onChange) {

View file

@ -39,6 +39,7 @@ const CellSelect = observer(class CellSelect extends React.Component<I.Cell, Sta
const { relation, getRecord, recordId, elementMapper, arrayLimit } = this.props;
const { isEditing } = this.state;
const record = getRecord(recordId);
const placeholder = this.props.placeholder || translate(`placeholderCell${relation.format}`);
const isSelect = relation.format == I.RelationType.Select;
const cn = [ 'wrap' ];
@ -49,7 +50,6 @@ const CellSelect = observer(class CellSelect extends React.Component<I.Cell, Sta
let value = this.getItems();
let content = null;
const placeholder = this.props.placeholder || translate(`placeholderCell${relation.format}`);
const length = value.length;
if (elementMapper) {
@ -385,8 +385,9 @@ const CellSelect = observer(class CellSelect extends React.Component<I.Cell, Sta
value = UtilCommon.arrayUnique(value);
if (maxCount && value.length > maxCount) {
value = value.slice(value.length - maxCount, value.length);
const length = value.length;
if (maxCount && (length > maxCount)) {
value = value.slice(length - maxCount, length);
};
if (onChange) {

View file

@ -708,7 +708,7 @@ const BlockEmbed = observer(class BlockEmbed extends React.Component<I.BlockComp
if (!iframe.length) {
iframe = $('<iframe />', {
id: 'receiver',
src: this.fixAsarPath(`./embed/iframe.html?theme=${commonStore.getThemeClass()}`),
src: UtilCommon.fixAsarPath(`./embed/iframe.html?theme=${commonStore.getThemeClass()}`),
frameborder: 0,
scrolling: 'no',
sandbox: sandbox.join(' '),
@ -970,18 +970,6 @@ const BlockEmbed = observer(class BlockEmbed extends React.Component<I.BlockComp
return Math.min(1, Math.max(0, w / rect.width));
};
fixAsarPath (path: string): string {
const origin = location.origin;
let href = location.href;
if (origin == 'file://') {
href = href.replace('/app.asar/', '/app.asar.unpacked/');
href = href.replace('/index.html', '/');
path = href + path.replace(/^\.\//, '');
};
return path;
};
onResizeInit () {
console.log('onResizeInit');
};

View file

@ -75,7 +75,7 @@ const BlockIconUser = observer(class BlockIconUser extends React.Component<I.Blo
onUpload () {
const { rootId } = this.props;
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
this.setState({ loading: true });
C.FileUpload(commonStore.space, '', paths[0], I.FileType.Image, (message: any) => {

View file

@ -42,7 +42,7 @@ const BlockAudio = observer(class BlockAudio extends React.Component<I.BlockComp
block={block}
icon="audio"
textFile={translate('blockAudioUpload')}
accept={Constant.extension.audio}
accept={Constant.fileExtension.audio}
onChangeUrl={this.onChangeUrl}
onChangeFile={this.onChangeFile}
readonly={readonly}

View file

@ -50,7 +50,7 @@ const BlockImage = observer(class BlockImage extends React.Component<I.BlockComp
block={block}
icon="image"
textFile={translate('blockImageUpload')}
accept={Constant.extension.image}
accept={Constant.fileExtension.image}
onChangeUrl={this.onChangeUrl}
onChangeFile={this.onChangeFile}
readonly={readonly}

View file

@ -79,7 +79,7 @@ const BlockPdf = observer(class BlockPdf extends React.Component<I.BlockComponen
block={block}
icon="pdf"
textFile={translate('blockPdfUpload')}
accept={Constant.extension.pdf}
accept={Constant.fileExtension.pdf}
onChangeUrl={this.onChangeUrl}
onChangeFile={this.onChangeFile}
readonly={readonly}
@ -224,7 +224,7 @@ const BlockPdf = observer(class BlockPdf extends React.Component<I.BlockComponen
const { content } = block;
const { hash } = content;
C.FileDownload(hash, window.Electron.tmpPath(), (message: any) => {
C.FileDownload(hash, UtilCommon.getElectron().tmpPath, (message: any) => {
if (message.path) {
Renderer.send('pathOpen', message.path);
};

View file

@ -54,7 +54,7 @@ const BlockVideo = observer(class BlockVideo extends React.Component<I.BlockComp
block={block}
icon="video"
textFile={translate('blockVideoUpload')}
accept={Constant.extension.video}
accept={Constant.fileExtension.video}
onChangeUrl={this.onChangeUrl}
onChangeFile={this.onChangeFile}
readonly={readonly}

View file

@ -1,4 +1,5 @@
import * as React from 'react';
import $ from 'jquery';
import { I, UtilCommon, Preview } from 'Lib';
import { Icon, Loader } from 'Component';

View file

@ -166,6 +166,7 @@ class Input extends React.Component<Props, State> {
this.isFocused = true;
keyboard.setFocus(true);
this.addClass('isFocused');
};
onBlur (e: any) {
@ -175,6 +176,7 @@ class Input extends React.Component<Props, State> {
this.isFocused = false;
keyboard.setFocus(false);
this.removeClass('isFocused');
};
onPaste (e: any) {
@ -263,19 +265,15 @@ class Input extends React.Component<Props, State> {
};
addClass (v: string) {
if (!this._isMounted) {
return;
if (this._isMounted) {
$(this.node).addClass(v);
};
$(this.node).addClass(v);
};
removeClass (v: string) {
if (!this._isMounted) {
return;
if (this._isMounted) {
$(this.node).removeClass(v);
};
$(this.node).removeClass(v);
};
setPlaceholder (v: string) {

View file

@ -100,7 +100,7 @@ class Select extends React.Component<Props, State> {
{current ? (
<React.Fragment>
{current.map((item: any, i: number) => (
<MenuItemVertical key={i} {...item} iconSize={item.iconSize ? 20 : undefined} />
<MenuItemVertical key={i} {...item} />
))}
<Icon className={acn.join(' ')} />
</React.Fragment>
@ -136,6 +136,7 @@ class Select extends React.Component<Props, State> {
for (const option of this.props.options) {
options.push(option);
};
return options;
};
@ -189,14 +190,14 @@ class Select extends React.Component<Props, State> {
element,
noFlipX: true,
onOpen: () => {
window.setTimeout(() => $(element).addClass('active'));
window.setTimeout(() => $(element).addClass('isFocused'));
if (onOpen) {
onOpen();
};
},
onClose: () => {
window.setTimeout(() => $(element).removeClass('active'));
window.setTimeout(() => $(element).removeClass('isFocused'));
if (onClose) {
onClose();

View file

@ -130,6 +130,7 @@ class Textarea extends React.Component<Props, State> {
};
keyboard.setFocus(true);
this.addClass('isFocused');
};
onBlur (e: any) {
@ -138,6 +139,7 @@ class Textarea extends React.Component<Props, State> {
};
keyboard.setFocus(false);
this.removeClass('isFocused');
};
onCopy (e: any) {
@ -181,12 +183,19 @@ class Textarea extends React.Component<Props, State> {
};
setError (v: boolean) {
if (!this._isMounted) {
return;
};
v ? this.addClass('withError') : this.removeClass('withError');
};
const node = $(this.node);
v ? node.addClass('withError') : node.removeClass('withError');
addClass (v: string) {
if (this._isMounted) {
$(this.node).addClass(v);
};
};
removeClass (v: string) {
if (this._isMounted) {
$(this.node).removeClass(v);
};
};
};

View file

@ -269,7 +269,7 @@ const MenuBlockCover = observer(class MenuBlockCover extends React.Component<I.M
const { data } = param;
const { onUpload, onUploadStart } = data;
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
close();
if (onUploadStart) {

View file

@ -383,14 +383,14 @@ const MenuOptionList = observer(class MenuOptionList extends React.Component<I.M
resize () {
const { getId, position, param } = this.props;
const { data, title } = param;
const { noFilter } = data;
const { data } = param;
const { noFilter, maxHeight } = data;
const items = this.getItems();
const obj = $(`#${getId()} .content`);
const offset = 16 + (noFilter ? 0 : 38);
const height = Math.max(HEIGHT + offset, Math.min(360, items.length * HEIGHT + offset));
const height = Math.max(HEIGHT + offset, Math.min(maxHeight || 360, items.length * HEIGHT + offset));
obj.css({ height: height });
obj.css({ height });
position();
};

View file

@ -65,7 +65,7 @@ class MenuHelp extends React.Component<I.Menu> {
};
getItems () {
const btn = <Button className="c16" text={window.Electron.version.app} />;
const btn = <Button className="c16" text={UtilCommon.getElectron().version.app} />;
return [
{ id: 'whatsNew', document: 'whatsNew', caption: btn },

View file

@ -444,6 +444,8 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
position () {
const { id, param } = this.props;
const { element, recalcRect, type, vertical, horizontal, fixedX, fixedY, isSub, noFlipX, noFlipY, withArrow } = param;
const borderTop = Number(window.AnytypeGlobalConfig?.menuBorderTop) || UtilCommon.sizeHeader();
const borderBottom = Number(window.AnytypeGlobalConfig?.menuBorderBottom) || 80;
if (this.ref && this.ref.beforePosition) {
this.ref.beforePosition();
@ -467,7 +469,6 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
const isFixed = (menu.css('position') == 'fixed') || (node.css('position') == 'fixed');
const offsetX = Number(typeof param.offsetX === 'function' ? param.offsetX() : param.offsetX) || 0;
const offsetY = Number(typeof param.offsetY === 'function' ? param.offsetY() : param.offsetY) || 0;
const minY = UtilCommon.sizeHeader();
let ew = 0;
let eh = 0;
@ -510,7 +511,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
y = oy - height + offsetY;
// Switch
if (!noFlipY && (y <= BORDER)) {
if (!noFlipY && (y <= borderTop)) {
y = oy + eh - offsetY;
};
break;
@ -523,7 +524,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
y = oy + eh + offsetY;
// Switch
if (!noFlipY && (y >= wh - height - 80)) {
if (!noFlipY && (y >= wh - height - borderBottom)) {
y = oy - height - offsetY;
};
break;
@ -563,8 +564,8 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
x = Math.max(BORDER, x);
x = Math.min(ww - width - BORDER, x);
y = Math.max(minY, y);
y = Math.min(wh - height - 80, y);
y = Math.max(borderTop, y);
y = Math.min(wh - height - borderBottom, y);
if (undefined !== fixedX) x = fixedX;
if (undefined !== fixedY) y = fixedY;

View file

@ -389,6 +389,7 @@ const MenuSearchObject = observer(class MenuSearchObject extends React.Component
const { filter, rootId, type, blockId, blockIds, position, onSelect, noClose } = data;
const addParam: any = data.addParam || {};
const object = detailStore.get(rootId, blockId);
const details = data.details || {};
if (!noClose) {
close();
@ -453,7 +454,7 @@ const MenuSearchObject = observer(class MenuSearchObject extends React.Component
addParam.onClick();
close();
} else {
UtilObject.create('', '', { name: filter, type: commonStore.type }, I.BlockPosition.Bottom, '', {}, [ I.ObjectFlag.SelectType, I.ObjectFlag.SelectTemplate ], (message: any) => {
UtilObject.create('', '', { name: filter, type: commonStore.type, ...details }, I.BlockPosition.Bottom, '', {}, [ I.ObjectFlag.SelectType, I.ObjectFlag.SelectTemplate ], (message: any) => {
UtilObject.getById(message.targetId, (object: any) => { process(object, true); });
close();
});

View file

@ -395,7 +395,7 @@ const MenuSelect = observer(class MenuSelect extends React.Component<I.Menu> {
resize () {
const { position, getId, param } = this.props;
const { data } = param;
const { noScroll, noVirtualisation } = data;
const { noScroll, maxHeight, noVirtualisation } = data;
const items = this.getItems(true);
const obj = $(`#${getId()}`);
const content = obj.find('.content');
@ -416,7 +416,7 @@ const MenuSelect = observer(class MenuSelect extends React.Component<I.Menu> {
height = items.reduce((res: number, current: any) => res + this.getRowHeight(current), height);
};
height = Math.min(370, height);
height = Math.min(maxHeight || 370, height);
height = Math.max(44, height);
content.css({ height });

View file

@ -601,7 +601,7 @@ class MenuSmile extends React.Component<I.Menu, State> {
close();
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
C.FileUpload(commonStore.space, '', paths[0], I.FileType.Image, (message: any) => {
if (!message.error.code && onUpload) {
onUpload(message.hash);

View file

@ -139,7 +139,7 @@ const Controls = observer(class Controls extends React.Component<Props, State> {
onIconUser () {
const { rootId } = this.props;
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
C.FileUpload(commonStore.space, '', paths[0], I.FileType.Image, (message: any) => {
if (message.hash) {
UtilObject.setIcon(rootId, '', message.hash);

View file

@ -21,7 +21,6 @@ const PageHeadEditor = observer(class PageHeadEditor extends React.Component<Pro
this.onScaleStart = this.onScaleStart.bind(this);
this.onScaleMove = this.onScaleMove.bind(this);
this.onScaleEnd = this.onScaleEnd.bind(this);
this.onClone = this.onClone.bind(this);
};
render (): any {
@ -129,19 +128,6 @@ const PageHeadEditor = observer(class PageHeadEditor extends React.Component<Pro
value.text(Math.ceil(v * 100) + '%');
};
onClone (e: any) {
const { rootId } = this.props;
const object = detailStore.get(rootId, rootId);
C.TemplateClone(rootId, (message: any) => {
if (message.id) {
UtilObject.openRoute({ id: message.id });
};
analytics.event('CreateTemplate', { objectType: object.targetObjectType });
});
};
});
export default PageHeadEditor;

View file

@ -25,7 +25,7 @@ const PageMainEmpty = observer(class PageMainEmpty extends React.Component<I.Pag
<Header component="mainEmpty" text={translate('commonSearch')} layout={I.ObjectLayout.SpaceView} {...this.props} />
<div className="wrapper">
<IconObject object={space} size={112} forceLetter={true} />
<IconObject object={space} size={96} forceLetter={true} />
<Title text={space.name} />
<Label text={translate('pageMainEmptyDescription')} />

View file

@ -255,7 +255,7 @@ const PageMainMedia = observer(class PageMainMedia extends React.Component<I.Pag
const block = blocks.find(it => it.isFile());
const { content } = block;
C.FileDownload(content.hash, window.Electron.tmpPath(), (message: any) => {
C.FileDownload(content.hash, UtilCommon.getElectron().tmpPath, (message: any) => {
if (message.path) {
Renderer.send('pathOpen', message.path);
};

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import { IconObject, Input, Title, Loader, Icon } from 'Component';
import { IconObject, Input, Title, Loader, Icon, Error } from 'Component';
import { I, C, translate, UtilCommon, Action, UtilObject, UtilRouter } from 'Lib';
import { authStore, detailStore, blockStore, menuStore, commonStore } from 'Store';
import { observer } from 'mobx-react';
@ -47,7 +47,7 @@ const PopupSettingsPageAccount = observer(class PopupSettingsPageAccount extends
return (
<div className="sections">
<div className="section top">
{error ? <div className="message">{error}</div> : ''}
<Error text={error} />
<div className="iconWrapper">
{loading ? <Loader /> : ''}
@ -132,7 +132,7 @@ const PopupSettingsPageAccount = observer(class PopupSettingsPageAccount extends
};
onUpload () {
Action.openFile(Constant.extension.cover, paths => {
Action.openFile(Constant.fileExtension.cover, paths => {
this.setState({ loading: true });
C.FileUpload(commonStore.space, '', paths[0], I.FileType.Image, (message: any) => {

View file

@ -1,6 +1,6 @@
import * as React from 'react';
import { Icon, Title, Label } from 'Component';
import { I, UtilCommon, translate, Action } from 'Lib';
import { I, UtilCommon, translate, Action, UtilMenu } from 'Lib';
import { observer } from 'mobx-react';
import Constant from 'json/constant.json';
import Head from '../head';
@ -46,7 +46,7 @@ const PopupSettingsPageImportIndex = observer(class PopupSettingsPageImportIndex
const common = [ I.ImportType.Html, I.ImportType.Text, I.ImportType.Protobuf, I.ImportType.Markdown ];
if (common.includes(item.format)) {
Action.import(item.format, Constant.extension.import[item.format]);
Action.import(item.format, Constant.fileExtension.import[item.format]);
close();
} else {
onPage(UtilCommon.toCamelCase('import-' + item.id));
@ -54,14 +54,7 @@ const PopupSettingsPageImportIndex = observer(class PopupSettingsPageImportIndex
};
getItems () {
return [
{ id: 'notion', name: 'Notion', format: I.ImportType.Notion },
{ id: 'markdown', name: 'Markdown', format: I.ImportType.Markdown },
{ id: 'html', name: 'HTML', format: I.ImportType.Html },
{ id: 'text', name: 'TXT', format: I.ImportType.Text },
{ id: 'protobuf', name: 'Any-Block', format: I.ImportType.Protobuf },
{ id: 'csv', name: 'CSV', format: I.ImportType.Csv },
];
return UtilMenu.getImportFormats();
};
});

View file

@ -203,18 +203,17 @@ const PopupSettingsSpaceIndex = observer(class PopupSettingsSpaceIndex extends R
C.WorkspaceCreate({ name, iconOption }, usecase, (message: any) => {
this.setState({ isLoading: false });
if (!message.error.code) {
analytics.event('CreateSpace', { usecase, middleTime: message.middleTime });
analytics.event('SelectUsecase', { type: usecase });
if (onCreate) {
onCreate(message.objectId);
};
close();
} else {
if (message.error.code) {
this.setState({ error: message.error.description });
return;
};
if (onCreate) {
onCreate(message.objectId);
};
analytics.event('CreateSpace', { usecase, middleTime: message.middleTime });
analytics.event('SelectUsecase', { type: usecase });
});
};

View file

@ -557,7 +557,7 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup,
// Import action
if (item.isImport) {
Action.import(item.format, Constant.extension.import[item.format]);
Action.import(item.format, Constant.fileExtension.import[item.format]);
// Buttons
} else {

View file

@ -135,8 +135,12 @@ const PopupSettingsOnboarding = observer(class PopupSettingsOnboarding extends R
this.props.close();
};
onPathClick (path: string) {
Renderer.send('pathOpen', window.Electron.dirname(path));
onPathClick () {
const { path } = this.config;
if (path) {
Renderer.send('pathOpen', UtilCommon.getElectron().dirname(path));
};
};
onChangeStorage () {

View file

@ -2,7 +2,7 @@ import * as React from 'react';
import raf from 'raf';
import { observer } from 'mobx-react';
import { Icon, ObjectName, DropTarget } from 'Component';
import { C, I, UtilCommon, UtilObject, UtilData, UtilMenu, translate, Storage, Action, analytics, Dataview } from 'Lib';
import { C, I, UtilCommon, UtilObject, UtilData, UtilMenu, translate, Storage, Action, analytics, Dataview, UtilDate } from 'Lib';
import { blockStore, detailStore, menuStore, dbStore, commonStore } from 'Store';
import Constant from 'json/constant.json';
@ -68,6 +68,7 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> {
getData: this.getData,
getLimit: this.getLimit,
sortFavorite: this.sortFavorite,
addGroupLabels: this.addGroupLabels,
};
if (className) {
@ -677,6 +678,61 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> {
return isPreview ? 0 : limit;
};
addGroupLabels (records: any[], widgetId: string) {
const now = UtilDate.now();
const { d, m, y } = UtilDate.getCalendarDateParam(now);
const today = now - UtilDate.timestamp(y, m, d);
const yesterday = now - UtilDate.timestamp(y, m, d - 1);
const lastWeek = now - UtilDate.timestamp(y, m, d - 7);
const lastMonth = now - UtilDate.timestamp(y, m - 1, d);
const groups = {
today: [],
yesterday: [],
lastWeek: [],
lastMonth: [],
older: []
};
let groupedRecords: I.WidgetTreeDetails[] = [];
let relationKey;
if (widgetId == Constant.widgetId.recentOpen) {
relationKey = 'lastOpenedDate';
};
if (widgetId == Constant.widgetId.recentEdit) {
relationKey = 'lastModifiedDate';
};
records.forEach((record) => {
const diff = now - record[relationKey];
if (diff < today) {
groups.today.push(record);
} else
if (diff < yesterday) {
groups.yesterday.push(record);
} else
if (diff < lastWeek) {
groups.lastWeek.push(record);
} else
if (diff < lastMonth) {
groups.lastMonth.push(record);
} else {
groups.older.push(record);
};
});
Object.keys(groups).forEach((key) => {
if (groups[key].length) {
groupedRecords.push({ id: key, type: '', links: [], isSection: true });
groupedRecords = groupedRecords.concat(groups[key]);
};
});
return groupedRecords;
};
});
export default WidgetIndex;

Some files were not shown because too many files have changed in this diff Show more