├── .gitignore ├── LICENSE ├── Pipfile ├── README.md ├── actions ├── __init__.py ├── demo1.py └── demo2.py ├── app.py ├── config.sample.ini ├── nplus ├── .gitignore ├── README.md ├── manifest.json ├── package.json ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ └── logo │ │ ├── get_started128.png │ │ ├── get_started16.png │ │ ├── get_started32.png │ │ ├── get_started48.png │ │ ├── img_0628_128.png │ │ ├── img_0628_16.png │ │ ├── img_0628_32.png │ │ └── img_0628_48.png ├── src │ ├── AppOptions.css │ ├── AppOptions.tsx │ ├── AppPopup.css │ ├── AppPopup.test.tsx │ ├── AppPopup.tsx │ ├── background.ts │ ├── components │ │ └── source.tsx │ ├── content-scripts │ │ ├── README.md │ │ ├── core.ts │ │ ├── msg.tsx │ │ └── plus.ts │ ├── index.css │ ├── options.tsx │ ├── popup.tsx │ └── react-app-env.d.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── run.sh └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | 4 | node_modules 5 | 6 | # pipenv install foo --skip-lock 7 | Pipfile.lock 8 | 9 | # config 10 | config.ini 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # celery beat schedule file 105 | celerybeat-schedule 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mayne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | autopep8 = "*" 8 | pylint = "*" 9 | 10 | [packages] 11 | flask = "*" 12 | flask-cors = "*" 13 | requests-html = "*" 14 | notion = {editable = true,git = "https://github.com/mayneyao/notion-py.git"} 15 | 16 | [requires] 17 | python_version = "3.7" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NotionPlus 2 | 3 | [https://www.notion.so/NotionPlus-71508def3f5045a2a4d68909cf0727fe](https://www.notion.so/NotionPlus-71508def3f5045a2a4d68909cf0727fe) 4 | 5 | ## Deployment 6 | 7 | ```shell 8 | 9 | cd nplus 10 | yarn 11 | 12 | // start dev server 13 | yarn start 14 | 15 | // build 16 | yarn build chrome 17 | // or 18 | yarn build firefox 19 | 20 | ``` 21 | 22 | ## Changelog 23 | 24 | see the [tags on this repository](https://github.com/mayneyao/NotionPlus/tags). 25 | 26 | ## Acknowledgments 27 | 28 | * [create-react-browser-extension](https://github.com/gxvv/create-react-browser-extension) - Easy to make chrome extension 29 | * [@jamalex/notion-py](https://github.com/jamalex/notion-py) 30 | 31 | ## License 32 | MIT 33 | -------------------------------------------------------------------------------- /actions/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | class Plus: 5 | def __init__(self): 6 | self.action_func_map = {} 7 | 8 | def action(self, name): 9 | def decorate(f): 10 | self.action_func_map[name] = f 11 | @wraps(f) 12 | def wrappers(*args, **kwds): 13 | print(args, kwds) 14 | return f(*args, **kwds) 15 | return wrappers 16 | return decorate 17 | 18 | 19 | notion_plus = Plus() 20 | 21 | action = notion_plus.action 22 | 23 | from .demo1 import * 24 | #from .demo2 import * -------------------------------------------------------------------------------- /actions/demo1.py: -------------------------------------------------------------------------------- 1 | from . import action 2 | 3 | import requests 4 | 5 | 6 | @action('test_action') 7 | def test_action(obj): 8 | obj.name = "完成了一项自动化后台任务" 9 | 10 | 11 | @action('change_tags') 12 | def changetags(obj): 13 | obj.tags = ['测试1'] 14 | 15 | 16 | @action('get_current_playing_music') 17 | def get_current_playing_music(obj): 18 | r = requests.get('https://api.gine.me/currently_playing').json() 19 | name = r['music']['item']['name'] 20 | obj.name = name 21 | 22 | # do you task here -------------------------------------------------------------------------------- /actions/demo2.py: -------------------------------------------------------------------------------- 1 | """ 2 | you can write task function here or anywhere 3 | but you should import this file in __int__.py 4 | `from .demo2 import *` at the end 5 | """ -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | from configparser import ConfigParser 3 | 4 | from flask import Flask, escape, request, Response 5 | from flask_cors import CORS 6 | from notion.client import NotionClient 7 | from notion.collection import CollectionQuery 8 | from notion.block import CodeBlock, EmbedOrUploadBlock 9 | from notion.utils import remove_signed_prefix_as_needed 10 | 11 | from actions import notion_plus 12 | 13 | 14 | conf = ConfigParser() 15 | conf.read('config.ini') 16 | token = conf.get('notion', 'token') 17 | timezone = conf.get('notion', 'timezone') 18 | auth_token = conf.get('security', 'auth_token') 19 | client = NotionClient(token_v2=token, timezone=timezone) 20 | 21 | app = Flask(__name__) 22 | CORS(app) 23 | 24 | 25 | def get_notion_action_code(action_name, action_table_url): 26 | actions_cv = client.get_collection_view(action_table_url) 27 | 28 | action_block = None 29 | q = CollectionQuery(actions_cv.collection, actions_cv, action_name) 30 | for row in q.execute(): 31 | print(row) 32 | action_block = row 33 | 34 | action_code = None 35 | for block in action_block.children: 36 | if isinstance(block, CodeBlock): 37 | action_code = block.title 38 | return action_code 39 | 40 | 41 | @app.route('/', methods=['POST']) 42 | def npp(): 43 | auth = request.headers.get('authtoken') 44 | if auth == auth_token: 45 | act = SimpleNamespace(**request.json) 46 | action_name = None 47 | obj = client.get_block(act.blockID) 48 | 49 | if act.actionName.startswith('#'): 50 | action_name = act.actionName.split('#')[-1] 51 | action_code = get_notion_action_code(action_name, act.actionTableUrl) 52 | if action_code: 53 | exec(action_code) 54 | else: 55 | return 'Task Code Not Found' 56 | elif act.actionName.startswith('@'): 57 | action_name = act.actionName.split('@')[-1] 58 | func = notion_plus.action_func_map[action_name] 59 | func(obj) 60 | setattr(obj, action_name, False) 61 | return 'Ok' 62 | else: 63 | return 'Bad Token' 64 | -------------------------------------------------------------------------------- /config.sample.ini: -------------------------------------------------------------------------------- 1 | [notion] 2 | token = get token_v2 from cookies 3 | timezone = Asia/Shanghai 4 | 5 | [security] 6 | auth_token = nobody knows but you -------------------------------------------------------------------------------- /nplus/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # development 12 | /dev 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /nplus/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) using the [browser-extension-react-scripts](https://github.com/gxvv/create-react-browser-extension). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start [vendor]` 8 | 9 | The vendor can be one of chrome, firefox, opera and edge. (default is chrome)
10 | Runs the app in the development mode.
11 | The compiler will generate a unpacked extension in the dev folder.
12 | The directory holding the manifest file can be added as an extension in developer mode in its current state. 13 | 14 | #### e.g Chrome 15 | 16 | 1. Open the Extension Management page by navigating to chrome://extensions. 17 | - The Extension Management page can also be opened by clicking on the Chrome menu, hovering over More Tools then selecting Extensions. 18 | 2. Enable Developer Mode by clicking the toggle switch next to Developer mode. 19 | 3. Click the LOAD UNPACKED button and select the extension directory. 20 | 21 | Open the popup to view it in the browser. 22 | 23 | The page will reload if you make edits.
24 | You will also see any lint errors in the console. 25 | 26 | ### `npm test` 27 | 28 | Launches the test runner in the interactive watch mode.
29 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 30 | 31 | ### `npm run build [vendor]` 32 | 33 | Builds the packed extension for distribution to the `build` folder.
34 | It correctly bundles React in production mode and optimizes the build for the best performance. 35 | 36 | The build is minified and the filenames include the hashes.
37 | Your extension is ready to be distributed! 38 | 39 | ### `npm run eject` 40 | 41 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 42 | 43 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 44 | 45 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 46 | 47 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 48 | 49 | ## Learn More 50 | 51 | You can learn more in the [Create React Browser Extension documentation](https://github.com/gxvv/create-react-browser-extension). 52 | 53 | To learn React, check out the [React documentation](https://reactjs.org/). 54 | -------------------------------------------------------------------------------- /nplus/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NotionPlus", 3 | "version": "2.0.3", 4 | "description": "Programming in Notion.so", 5 | "permissions": [ 6 | "storage" 7 | ], 8 | "__firefox__browser_specific_settings": { 9 | "gecko": { 10 | "id": "i@gine.me" 11 | } 12 | }, 13 | "__chrome__options_page": "options.html", 14 | "__firefox__options_ui": { 15 | "page": "options.html", 16 | "browser_style": true 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "*://*.notion.so/*" 22 | ], 23 | "js": [ 24 | "/static/js/content-script-plus.js" 25 | ], 26 | "run_at": "document_end" 27 | } 28 | ], 29 | "icons": { 30 | "16": "logo/img_0628_16.png", 31 | "32": "logo/img_0628_32.png", 32 | "48": "logo/img_0628_48.png", 33 | "128": "logo/img_0628_128.png" 34 | }, 35 | "manifest_version": 2 36 | } -------------------------------------------------------------------------------- /nplus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nplus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.3", 7 | "@material-ui/icons": "^4.9.1", 8 | "@material-ui/lab": "^4.0.0-alpha.43", 9 | "@types/react": "^16.9.22", 10 | "@types/react-dom": "^16.9.5", 11 | "browser-extension-react-scripts": "1.0.1-alpha.5", 12 | "cra-template-typescript": "1.0.2", 13 | "notabase": "^0.9.5", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "typescript": "^3.8.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app", 26 | "env": { 27 | "browser": true, 28 | "webextensions": true 29 | } 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nplus/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/favicon.ico -------------------------------------------------------------------------------- /nplus/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/favicon.png -------------------------------------------------------------------------------- /nplus/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | NotionPlus 16 | 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /nplus/public/logo/get_started128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/get_started128.png -------------------------------------------------------------------------------- /nplus/public/logo/get_started16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/get_started16.png -------------------------------------------------------------------------------- /nplus/public/logo/get_started32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/get_started32.png -------------------------------------------------------------------------------- /nplus/public/logo/get_started48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/get_started48.png -------------------------------------------------------------------------------- /nplus/public/logo/img_0628_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/img_0628_128.png -------------------------------------------------------------------------------- /nplus/public/logo/img_0628_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/img_0628_16.png -------------------------------------------------------------------------------- /nplus/public/logo/img_0628_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/img_0628_32.png -------------------------------------------------------------------------------- /nplus/public/logo/img_0628_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayneyao/NotionPlus/6701d03a10ef5f8149c3c49d409c1d5333da49a3/nplus/public/logo/img_0628_48.png -------------------------------------------------------------------------------- /nplus/src/AppOptions.css: -------------------------------------------------------------------------------- 1 | .main { 2 | max-width: 600px; 3 | margin: 0 auto 4 | } -------------------------------------------------------------------------------- /nplus/src/AppOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, Theme } from "@material-ui/core/styles"; 3 | import Tabs from "@material-ui/core/Tabs"; 4 | import Tab from "@material-ui/core/Tab"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import Box from "@material-ui/core/Box"; 7 | import SourceSettings from './components/source'; 8 | 9 | interface TabPanelProps { 10 | children?: React.ReactNode; 11 | index: any; 12 | value: any; 13 | } 14 | 15 | function TabPanel(props: TabPanelProps) { 16 | const { children, value, index, ...other } = props; 17 | 18 | return ( 19 | 30 | ); 31 | } 32 | 33 | function a11yProps(index: any) { 34 | return { 35 | id: `vertical-tab-${index}`, 36 | "aria-controls": `vertical-tabpanel-${index}` 37 | }; 38 | } 39 | 40 | const useStyles = makeStyles((theme: Theme) => ({ 41 | root: { 42 | flexGrow: 1, 43 | backgroundColor: theme.palette.background.paper, 44 | display: "flex", 45 | height: "100vh", 46 | width: 900, 47 | margin: "0 auto" 48 | }, 49 | tabs: { 50 | borderRight: `1px solid ${theme.palette.divider}`, 51 | height: "100%", 52 | display: "flex", 53 | } 54 | })); 55 | 56 | export default function Setting() { 57 | const classes = useStyles(); 58 | const [value, setValue] = React.useState(0); 59 | 60 | const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { 61 | setValue(newValue); 62 | }; 63 | 64 | return ( 65 |
66 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 99 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /nplus/src/AppPopup.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | width: 30px; 4 | height: 30px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 50px; 9 | height: 50px; 10 | } 11 | 12 | button { 13 | height: 30px; 14 | width: 30px; 15 | outline: none; 16 | } 17 | -------------------------------------------------------------------------------- /nplus/src/AppPopup.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /nplus/src/AppPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './AppPopup.css'; 3 | 4 | export default class AppPopup extends React.Component { 5 | constructor(props: any) { 6 | super(props); 7 | 8 | this.state = { 9 | color: '' 10 | }; 11 | } 12 | 13 | componentDidMount() { 14 | this.initColor(); 15 | } 16 | 17 | initColor = async () => { 18 | const data = await browser.storage.sync.get('color'); 19 | 20 | this.setState({ 21 | color: data.color 22 | }); 23 | } 24 | 25 | handleButtonClick = async () => { 26 | const { color } = this.state; 27 | const tabs = await browser.tabs.query({ 28 | active: true, 29 | currentWindow: true 30 | }); 31 | 32 | browser.tabs.executeScript( 33 | tabs[0].id, 34 | { 35 | code: `document.body.style.backgroundColor = "${color}";` 36 | } 37 | ); 38 | } 39 | 40 | render() { 41 | const { color } = this.state; 42 | 43 | return ( 44 |
45 |
52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nplus/src/background.ts: -------------------------------------------------------------------------------- 1 | browser.runtime.onInstalled.addListener(() => { 2 | browser.storage.sync.set({ color: '#3aa757' }).then(() => { 3 | console.log('The color is green.'); 4 | }); 5 | }); 6 | 7 | export default {}; -------------------------------------------------------------------------------- /nplus/src/components/source.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import Button from '@material-ui/core/Button'; 4 | 5 | export default function Settings() { 6 | const initState = { 7 | serverHost: '', 8 | authToken: '', 9 | actionTableUrl: '' 10 | } 11 | const [settings, setSettings] = useState(initState) 12 | 13 | useEffect(() => { 14 | initSettings() 15 | }, []) 16 | 17 | const initSettings = async () => { 18 | const data = await browser.storage.sync.get(['serverHost', 'authToken', 'actionTableUrl']); 19 | 20 | const { serverHost, authToken, actionTableUrl } = data 21 | setSettings({ 22 | serverHost, 23 | authToken, 24 | actionTableUrl 25 | }); 26 | } 27 | 28 | const handleChange = (name: any) => (event: any) => { 29 | setSettings({ ...settings, [name]: event!.target!.value }); 30 | }; 31 | 32 | const handleSubmit = () => { 33 | // const { serverHost, authToken, actionTableUrl } = this.state 34 | browser.storage.sync.set(settings).then(() => { 35 | // console.log(serverHost, authToken, actionTableUrl); 36 | console.log(settings) 37 | }); 38 | } 39 | const { serverHost, authToken, actionTableUrl } = settings 40 | 41 | return
42 |

Notion

43 |
44 |
45 | 55 |
56 |
57 |

ServerHost (Optional)

58 | If you write task code in Python, you need to configure this 59 |
60 |
61 | 71 |
72 |
73 | 83 |
84 |
85 | 88 |
89 | } -------------------------------------------------------------------------------- /nplus/src/content-scripts/README.md: -------------------------------------------------------------------------------- 1 | # content scripts option 2 | 3 | Put all content scripts in this folder. The compiled content scripts will be generated in `static/js` folder with prefix **content-script-**. Such as the `src/content-scripts/example.ts` will be `/static/js/content-script-example.js`. Config the option in `manifest.json` use the new file name. -------------------------------------------------------------------------------- /nplus/src/content-scripts/core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action 执行相关逻辑 3 | */ 4 | import Notabase, { getFullBlockId } from 'notabase'; 5 | import { showMsg, MsgHorizontalType } from './msg'; 6 | 7 | const nb = new Notabase() 8 | 9 | export interface IActionCode { 10 | [name: string]: { 11 | code?: string; 12 | lang?: 'JavaScript' | 'Python' 13 | hasChildrenTask: boolean; 14 | children?: string[]; 15 | childrenType?: string; 16 | isGlobal?: boolean; 17 | } 18 | } 19 | 20 | export interface IEnv { 21 | [key: string]: string 22 | } 23 | 24 | // TODO: 环境变量 25 | export const getEnv = async () => { 26 | const ENV: IEnv = {} 27 | const data = await browser.storage.sync.get(['envTableUrl']) 28 | const { envTableUrl } = data 29 | const envs = await nb.fetch(envTableUrl) 30 | envs.rows.forEach(row => { 31 | const { Key, Value } = row 32 | ENV[Key as string] = Value as string 33 | }) 34 | return ENV; 35 | 36 | } 37 | 38 | export const getAllActionCode = async () => { 39 | const actionCode: IActionCode = {} 40 | const data = await browser.storage.sync.get(['serverHost', 'authToken', 'actionTableUrl']) 41 | let { actionTableUrl } = data; 42 | if (actionTableUrl) { 43 | const actions = await nb.fetch(actionTableUrl) 44 | actions.rows.forEach((actionRow: any) => { 45 | let hasChildrenTask = Boolean(actionRow.Children) 46 | const actionRowName: string = actionRow && actionRow.Name 47 | if (hasChildrenTask && actionRowName) { 48 | actionCode[actionRowName] = { 49 | hasChildrenTask, 50 | children: actionRow.Children.filter((r: any) => r).map((r: any) => r.Name), 51 | childrenType: actionRow.ChildrenType, 52 | isGlobal: actionRow.IsGlobal, 53 | } 54 | } else { 55 | let codeBlockId = actionRow.content && actionRow.content[0] 56 | if (codeBlockId) { 57 | // eslint-disable-next-line 58 | const { type, properties } = nb.blockStore[codeBlockId].value 59 | let code = properties && properties.title[0][0] 60 | let lang = properties && properties.language[0][0] 61 | actionCode[actionRow.Name] = { 62 | code, 63 | lang, 64 | hasChildrenTask, 65 | isGlobal: actionRow.IsGlobal, 66 | } 67 | } 68 | } 69 | }) 70 | console.log("action code is ready") 71 | console.log(actionCode) 72 | } else { 73 | return; 74 | } 75 | return actionCode 76 | } 77 | 78 | 79 | interface ActionParams { 80 | actionCode: IActionCode; 81 | blockID?: string; 82 | actionName: string; 83 | actionParams?: any[] 84 | } 85 | export const doAction = async ({ actionCode, blockID, actionName, actionParams }: ActionParams) => { 86 | console.log('exec action', actionName, actionParams) 87 | const parsedBlockID = blockID ? getFullBlockId(blockID) : undefined 88 | console.log(parsedBlockID) 89 | const data = await browser.storage.sync.get(['serverHost', 'authToken', 'actionTableUrl']) 90 | let { serverHost, authToken, actionTableUrl } = data; 91 | 92 | if (actionCode.hasOwnProperty(actionName)) { 93 | // js 代码浏览器运行 94 | let func = actionCode[actionName] 95 | 96 | console.log(func); 97 | if (func.hasChildrenTask) { 98 | switch (func.childrenType) { 99 | case "串": 100 | for (let taskName of func.children!) { 101 | await doAction({ 102 | actionCode, 103 | actionName: taskName, 104 | blockID: parsedBlockID, 105 | actionParams, 106 | }) 107 | } 108 | break 109 | case "并": 110 | func.children!.map(async taskName => { 111 | await doAction({ 112 | actionCode, 113 | actionName: taskName, 114 | blockID: parsedBlockID, 115 | actionParams, 116 | }) 117 | }) 118 | break 119 | } 120 | } else { 121 | switch (func.lang) { 122 | case "JavaScript": 123 | const code = func.code 124 | if (!code) return; 125 | try { 126 | console.log(actionParams); 127 | const _actionParams = actionParams ? actionParams : []; 128 | if (func.isGlobal) { 129 | nb.startAtomic(); 130 | // eslint-disable-next-line 131 | const funcBody = eval(code) 132 | console.log("exec global action"); 133 | const res = await funcBody(..._actionParams) 134 | nb.endAtomic(); 135 | return res 136 | } else { 137 | // 非全局 action 才有上下文 138 | // this table 139 | let table = await nb.fetch(window.location.href) 140 | // this row 141 | let records = table.rows.filter((o: any) => o.id === parsedBlockID) 142 | console.log("obj is >>>>>", records); 143 | if (records.length === 1) { 144 | console.log("action applay on one row"); 145 | // eslint-disable-next-line 146 | const funcBody = eval(code) 147 | return await funcBody(..._actionParams) 148 | // return showMsg(`${actionCode} is not a function`); 149 | } else { 150 | console.log("action applay on all rows"); 151 | nb.startAtomic(); 152 | records = table.rows; 153 | // eslint-disable-next-line 154 | const funcBody = eval(code) 155 | const res = await funcBody(..._actionParams) 156 | nb.endAtomic(); 157 | return res; 158 | // return showMsg(`${actionCode} is not a function`); 159 | } 160 | } 161 | } catch (error) { 162 | console.log(error) 163 | showMsg("oops~ something error\n checkout devtools console", MsgHorizontalType.left) 164 | } 165 | break 166 | case "Python": 167 | // python 代码走服务器运行 168 | if (!serverHost || !authToken) { 169 | alert("⚠️ 请在配置页中配置服务器地址和安全码") 170 | } else { 171 | if (actionName.startsWith("#") && !actionTableUrl) { 172 | alert("⚠️ 您正在执行一项动态任务,但是动态任务表格地址没有正确配置,请在配置页中完善") 173 | } else { 174 | fetch(serverHost, { 175 | method: "POST", 176 | body: JSON.stringify({ 177 | actionName, 178 | blockID: parsedBlockID, 179 | actionTableUrl 180 | }), 181 | headers: new Headers({ 182 | 'Content-Type': 'application/json', 183 | 'authtoken': `${authToken}` 184 | }) 185 | }).then(res => { 186 | if (res.status === 200) { 187 | } else { 188 | console.log(`执行动作 ${actionName} 服务器遇到问题: ${res.statusText}`) 189 | } 190 | }) 191 | } 192 | } 193 | break 194 | } 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /nplus/src/content-scripts/msg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Snackbar from '@material-ui/core/Snackbar'; 3 | import { render } from 'react-dom' 4 | 5 | 6 | export enum MsgHorizontalType { 7 | left = 'left', 8 | right = 'right' 9 | } 10 | 11 | export const showMsg = (msg: string, horizontal: MsgHorizontalType = MsgHorizontalType.right) => { 12 | const msgDivId = horizontal === MsgHorizontalType.right ? 'NotionPlusUserMsg' : 'NotionPlusMsg' 13 | const msgBoxDiv = document.getElementById(msgDivId) 14 | if (msgBoxDiv) { 15 | msgBoxDiv.remove() 16 | } 17 | const reactRoot = document.createElement('div') 18 | reactRoot.setAttribute('id', msgDivId) 19 | document.body.append(reactRoot) 20 | render(React.createElement(Msg, { msg, horizontal }), reactRoot) 21 | } 22 | 23 | export const Msg: React.FC<{ msg: string, horizontal: MsgHorizontalType }> = ({ msg, horizontal }) => { 24 | const [open, setOpen] = React.useState(true); 25 | const handleClose = (event: React.SyntheticEvent | React.MouseEvent, reason?: string) => { 26 | if (reason === 'clickaway') { 27 | return; 28 | } 29 | setOpen(false); 30 | const msgMountNode = document.getElementById('NotionPlusMsg') 31 | if (msgMountNode) { 32 | msgMountNode.remove() 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /nplus/src/content-scripts/plus.ts: -------------------------------------------------------------------------------- 1 | import { doAction, getAllActionCode } from './core' 2 | import { showMsg, MsgHorizontalType } from './msg' 3 | import Notabase from 'notabase' 4 | 5 | 6 | // 注册控制台和 Action 可调用的全局对象 7 | declare global { 8 | interface Window { 9 | nb: Notabase; 10 | showMsg: (s: string, t: MsgHorizontalType) => void; 11 | } 12 | } 13 | 14 | window.nb = new Notabase() 15 | window.showMsg = showMsg; 16 | 17 | const getNotionContext = () => { 18 | const pathNameList = window.location.pathname.split('/') 19 | const currentPageId = pathNameList[pathNameList.length - 1] 20 | const search = new URLSearchParams(window.location.search) 21 | const selectedRecordId = search.get('p') 22 | const selectedViewId = search.get('v') 23 | return { 24 | currentURL: window.location.href, 25 | currentPageId, 26 | selectedRecordId: selectedViewId && selectedRecordId ? selectedRecordId : currentPageId, 27 | } 28 | } 29 | 30 | 31 | // interface IActionRes { 32 | // success: boolean; 33 | // msg: string; 34 | // data: { 35 | // icon: string; 36 | // title: string; 37 | // desc?: string; 38 | // action?: () => void; 39 | // } 40 | // } 41 | 42 | // const onActionDone = (res?: IActionRes) => { 43 | // if (res?.success) { 44 | // console.log("任务执行完毕") 45 | // } 46 | // } 47 | 48 | const doActionWrapper = ({ actionCode, actionName, actionParams }: any) => { 49 | console.log(actionCode); 50 | console.log("ready to exec", actionName, actionParams) 51 | if (actionCode && actionCode[actionName]) { 52 | const notionContext = getNotionContext(); 53 | doAction({ 54 | actionName, 55 | actionParams, 56 | actionCode, 57 | blockID: notionContext.selectedRecordId, 58 | }).then(res => { 59 | console.log("action res", res) 60 | showMsg(`Action: ${actionName} done ✔`, MsgHorizontalType.left) 61 | // onActionDone(res); 62 | }) 63 | showMsg(`Exec Action: ${actionName}`, MsgHorizontalType.left); 64 | } else { 65 | showMsg(`Action: ${actionName} Not Found!`, MsgHorizontalType.left); 66 | console.log(`Action: ${actionName} Not Found!`) 67 | } 68 | } 69 | 70 | 71 | const handleKeyPress = (e: KeyboardEvent, actionCode: any) => { 72 | if ((e.altKey) && e.key === "Enter") { 73 | const inputValue = (document.activeElement as HTMLInputElement).value! 74 | const inputArray = inputValue.split(" "); 75 | const action = inputArray[0] 76 | const params = inputArray.slice(1) 77 | doActionWrapper({ 78 | actionCode, 79 | actionName: action, 80 | actionParams: params 81 | }); 82 | const quikFindEle = document.querySelector(".notion-quick-find-menu"); 83 | // wa 84 | if (quikFindEle && quikFindEle.parentElement && 85 | quikFindEle.parentElement.parentElement && 86 | quikFindEle.parentElement.parentElement.parentElement) { 87 | quikFindEle.parentElement.parentElement.parentElement.innerHTML = "" 88 | } 89 | } 90 | 91 | // 快速搜索窗口下,选中的结果,按住右箭头快速填充到输入框 92 | if (e.code === "ArrowRight") { 93 | const searchRes = document.querySelectorAll(`#notion-app > div > div.notion-overlay-container.notion-default-overlay-container > 94 | div > div > div > div > div > div > section > div > div > div`) 95 | const recentRes = document.querySelectorAll(`#notion-app > div > div.notion-overlay-container.notion-default-overlay-container > 96 | div > div > div > div > div > div > main > div > div > ul > div`) 97 | let res = searchRes; 98 | if (searchRes.length) res = searchRes; 99 | if (recentRes.length && !searchRes.length) res = recentRes; 100 | 101 | const selectItem = res && Array.from(res).find(item => (item as HTMLLIElement).style.backgroundColor === "rgba(55, 53, 47, 0.08)") 102 | if (selectItem) { 103 | const selectPageText = (selectItem!.firstChild!.childNodes[1].firstChild! as HTMLLIElement).innerText; 104 | // console.log(selectPageText); 105 | (document.activeElement as HTMLInputElement).value = selectPageText 106 | e.preventDefault(); 107 | e.stopImmediatePropagation(); 108 | } 109 | } 110 | } 111 | 112 | const loadNotionPlus = (actionCode: any) => { 113 | const reactRoot = document.createElement('div') 114 | reactRoot.setAttribute('id', 'notionplus') 115 | document.body.append(reactRoot) 116 | document.addEventListener('keyup', (e) => handleKeyPress(e, actionCode)) 117 | } 118 | 119 | (async () => { 120 | const actionCode = await getAllActionCode() 121 | if (!actionCode) { 122 | console.warn("actions can't load, You need to config ActionTableUrl") 123 | console.warn("请在 NotionPlus 选项配置页中填入 ActionTableUrl,否则插件不会正常工作") 124 | } 125 | loadNotionPlus(actionCode) 126 | console.log('NotionPlus V2.0.3'); 127 | // console.log('try `const data = await nb.fetch(window.location.href)` via browser console'); 128 | })(); -------------------------------------------------------------------------------- /nplus/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /nplus/src/options.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import AppOptions from './AppOptions'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /nplus/src/popup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import AppPopup from './AppPopup'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /nplus/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /nplus/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "aplayer": "^1.10.1", 4 | "react-player": "^1.15.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | pipenv run env FLASK_APP=app.py flask run --host="0.0.0.0" -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | aplayer@^1.10.1: 6 | version "1.10.1" 7 | resolved "https://registry.yarnpkg.com/aplayer/-/aplayer-1.10.1.tgz#318289206107452cc39e8f552fa6cc6cb459a90c" 8 | integrity sha512-HAfyxgCUTLAqtYlxzzK9Fyqg6y+kZ9CqT1WfeWE8FSzwspT6oBqWOZHANPHF3RGTtC33IsyEgrfthPDzU5r9kQ== 9 | dependencies: 10 | balloon-css "^0.5.0" 11 | promise-polyfill "7.1.0" 12 | smoothscroll "0.4.0" 13 | 14 | balloon-css@^0.5.0: 15 | version "0.5.2" 16 | resolved "https://registry.yarnpkg.com/balloon-css/-/balloon-css-0.5.2.tgz#9e2163565a136c9d4aa20e8400772ce3b738d3ff" 17 | integrity sha512-zheJpzwyNrG4t39vusA67v3BYg1HTVXOF8cErPEHzWK88PEOFwgo6Ea9VHOgOWNMgeuOtFVtB73NE2NWl9uDyQ== 18 | 19 | deepmerge@^4.0.0: 20 | version "4.2.2" 21 | resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" 22 | integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== 23 | 24 | "js-tokens@^3.0.0 || ^4.0.0": 25 | version "4.0.0" 26 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 27 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 28 | 29 | load-script@^1.0.0: 30 | version "1.0.0" 31 | resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" 32 | integrity sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ= 33 | 34 | loose-envify@^1.4.0: 35 | version "1.4.0" 36 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 37 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 38 | dependencies: 39 | js-tokens "^3.0.0 || ^4.0.0" 40 | 41 | object-assign@^4.1.1: 42 | version "4.1.1" 43 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 44 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 45 | 46 | promise-polyfill@7.1.0: 47 | version "7.1.0" 48 | resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.0.tgz#4d749485b44577c14137591c6f36e5d7e2dd3378" 49 | integrity sha512-P6NJ2wU/8fac44ENORsuqT8TiolKGB2u0fEClPtXezn7w5cmLIjM/7mhPlTebke2EPr6tmqZbXvnX0TxwykGrg== 50 | 51 | prop-types@^15.7.2: 52 | version "15.7.2" 53 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" 54 | integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 55 | dependencies: 56 | loose-envify "^1.4.0" 57 | object-assign "^4.1.1" 58 | react-is "^16.8.1" 59 | 60 | react-is@^16.8.1: 61 | version "16.12.0" 62 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" 63 | integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== 64 | 65 | react-player@^1.15.2: 66 | version "1.15.2" 67 | resolved "https://registry.yarnpkg.com/react-player/-/react-player-1.15.2.tgz#b348df962dbdba39242e8c6edef1df479531a00b" 68 | integrity sha512-8KWo2ZQU9OnTx5Yp7eKRh/jGadSc436MOqJ+7c7RryfOXeuY938yZ2Osvh6bZY+etMWXDBHqXm14Vv9SoGb68g== 69 | dependencies: 70 | deepmerge "^4.0.0" 71 | load-script "^1.0.0" 72 | prop-types "^15.7.2" 73 | 74 | smoothscroll@0.4.0: 75 | version "0.4.0" 76 | resolved "https://registry.yarnpkg.com/smoothscroll/-/smoothscroll-0.4.0.tgz#40e507b46461408ba1b787d0081e1e883c4124a5" 77 | integrity sha512-sggQ3U2Un38b3+q/j1P4Y4fCboCtoUIaBYoge+Lb6Xg1H8RTIif/hugVr+ErMtIDpvBbhQfTjtiTeYAfbw1ZGQ== 78 | --------------------------------------------------------------------------------