├── .babelrc
├── .doolrc
├── .eslintrc
├── .gitignore
├── README.md
├── build
├── clean.js
├── index.js
└── main.js
├── images
└── image.png
├── package.json
└── src
├── app.html
├── components
├── App.js
├── Header.js
├── List.js
├── Mount.js
└── NoData.js
├── icons
├── app.icns
├── icon.less
├── iconfont.eot
├── iconfont.svg
├── iconfont.ttf
└── iconfont.woff
├── index.js
├── index.less
├── main.js
└── menu.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ['env', 'stage-2', 'react'],
3 | "plugins": ["transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/.doolrc:
--------------------------------------------------------------------------------
1 | {
2 | babelPlugins: ['transform-runtime']
3 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | parser: babel-eslint
2 | extends: airbnb
3 |
4 | env:
5 | browser: true
6 |
7 | rules:
8 | camelcase: 0
9 | comma-dangle: 0
10 | semi: [2, always]
11 | no-underscore-dangle: 0
12 | object-curly-spacing: [2, always]
13 | import/no-extraneous-dependencies: 0
14 | react/no-danger: 0
15 | react/forbid-prop-types: 0
16 | react/jsx-filename-extension: 0
17 | jsx-a11y/href-no-hash: 0
18 | jsx-a11y/no-static-element-interactions: 0
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea/
3 | .ipr
4 | .iws
5 | *~
6 | ~*
7 | *.diff
8 | *.patch
9 | *.bak
10 | .DS_Store
11 | Thumbs.db
12 | .project
13 | .*proj
14 | .svn/
15 | *.swp
16 | *.swo
17 | *.log
18 | *.sublime-project
19 | *.sublime-workspace
20 |
21 | npm-debug.log
22 | package-lock.json
23 | node_modules
24 |
25 | .buildpath
26 | .settings
27 | coverage
28 | .nyc_output
29 |
30 | app/
31 | dist/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Disky
2 | =====
3 |
4 | > Make NTFS writable on macOS
5 |
6 | 
7 |
8 | ## Development
9 |
10 | ```bash
11 | npm i dool -g
12 | npm i
13 |
14 | npm run dev
15 | npm run start
16 |
17 | # Electron Mirror of China
18 | ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
19 |
20 | npm run pack
21 | npm run disk
22 | ```
23 |
24 | ## Report a issue
25 |
26 | * [All issues](https://github.com/d-band/disky/issues)
27 | * [New issue](https://github.com/d-band/disky/issues/new)
28 |
29 | ## License
30 |
31 | Disky is available under the terms of the MIT License.
32 |
--------------------------------------------------------------------------------
/build/clean.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const join = require('path').join;
3 | const rimraf = require('rimraf');
4 |
5 | const dist = join(__dirname, '../dist');
6 | rimraf(dist, () => {
7 | fs.mkdirSync(dist);
8 | const html = fs.readFileSync(
9 | join(__dirname, '../src/app.html'),
10 | 'utf8'
11 | );
12 | fs.writeFileSync(
13 | join(dist, 'app.html'),
14 | html.replace(/http:\/\/localhost:8000/g, '.'),
15 | 'utf8'
16 | );
17 | });
18 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | config.target = 'electron-renderer';
3 | config.entry = {
4 | index: './src/index.js'
5 | };
6 | return config;
7 | }
--------------------------------------------------------------------------------
/build/main.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | config.target = 'electron-main';
3 | config.entry = {
4 | main: './src/main.js'
5 | };
6 | config.node = {
7 | __dirname: false,
8 | __filename: false
9 | };
10 | return config;
11 | }
--------------------------------------------------------------------------------
/images/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/images/image.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "disky",
3 | "version": "1.0.0",
4 | "description": "NTFS writable for macOS",
5 | "main": "dist/main.js",
6 | "scripts": {
7 | "lint": "eslint --ext .js src",
8 | "dev": "dool server --config build/index.js",
9 | "start": "NODE_ENV=dev electron -r babel-register ./src/main.js",
10 | "build-main": "dool build --no-compress --config build/main.js",
11 | "build-index": "dool build --config build/index.js",
12 | "prebuild": "node build/clean.js",
13 | "build": "npm run build-index && npm run build-main",
14 | "pack": "npm run build && rimraf app && electron-builder --dir",
15 | "dist": "npm run build && rimraf app && electron-builder"
16 | },
17 | "build": {
18 | "appId": "com.dband.disky",
19 | "productName": "Disky",
20 | "directories": {
21 | "output": "app"
22 | },
23 | "files": [
24 | "dist/**/*",
25 | "app.html"
26 | ],
27 | "mac": {
28 | "category": "public.app-category.developer-tools",
29 | "icon": "src/icons/app.icns"
30 | }
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/d-band/disky.git"
35 | },
36 | "keywords": [
37 | "NTFS",
38 | "macOS"
39 | ],
40 | "author": "d-band",
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/d-band/disky/issues"
44 | },
45 | "homepage": "https://github.com/d-band/disky#readme",
46 | "devDependencies": {
47 | "babel-eslint": "^8.0.0",
48 | "babel-plugin-transform-runtime": "^6.23.0",
49 | "babel-preset-env": "^1.6.0",
50 | "babel-preset-react": "^6.24.1",
51 | "babel-preset-stage-2": "^6.24.1",
52 | "babel-register": "^6.26.0",
53 | "electron": "^1.7.6",
54 | "electron-builder": "^19.27.7",
55 | "eslint": "^4.7.1",
56 | "eslint-config-airbnb": "^15.1.0",
57 | "eslint-plugin-import": "^2.7.0",
58 | "eslint-plugin-jsx-a11y": "^5.1.1",
59 | "eslint-plugin-react": "^7.3.0",
60 | "rimraf": "^2.6.2"
61 | },
62 | "dependencies": {
63 | "ls-usb": "0.1.0",
64 | "prop-types": "^15.5.10",
65 | "react": "^15.6.1",
66 | "react-dom": "^15.6.1",
67 | "react-redux": "^5.0.6",
68 | "sudo-prompt": "^7.1.1",
69 | "yax": "^0.3.1"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Disky
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { mapState, mapActions } from 'yax';
5 | import List from './List';
6 | import Mount from './Mount';
7 |
8 | class App extends Component {
9 | static propTypes = {
10 | mode: PropTypes.string.isRequired,
11 | page: PropTypes.string.isRequired,
12 | getMedias: PropTypes.func.isRequired
13 | };
14 | componentWillMount() {
15 | this.props.getMedias();
16 | }
17 | render() {
18 | const { mode, page } = this.props;
19 | return (
20 |
21 | {page === 'list' ?
: }
22 |
23 | );
24 | }
25 | }
26 |
27 | export default connect(
28 | mapState({ mode: 'mode', page: 'page' }),
29 | mapActions({ getMedias: 'getMedias' })
30 | )(App);
31 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { ipcRenderer, remote } from 'electron';
5 |
6 | function Header({ mode, dispatch }) {
7 | const refresh = () => dispatch({
8 | type: 'getMedias'
9 | });
10 | const openMenu = () => {
11 | ipcRenderer.send('open-menu', mode);
12 | };
13 | const hide = () => {
14 | remote.app.hide();
15 | };
16 | return (
17 |
18 |
21 |
Disky
22 |
26 |
27 | );
28 | }
29 |
30 | Header.propTypes = {
31 | mode: PropTypes.string.isRequired,
32 | dispatch: PropTypes.func.isRequired
33 | };
34 |
35 | export default connect(({ mode }) => ({ mode }))(Header);
36 |
--------------------------------------------------------------------------------
/src/components/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { shell } from 'electron';
5 | import { mapState } from 'yax';
6 | import Header from './Header';
7 | import NoData from './NoData';
8 |
9 | function List({ list, dispatch }) {
10 | const eject = (v) => {
11 | if (!v.mount) return;
12 | dispatch({ type: 'eject', payload: v.node });
13 | };
14 | const open = (v) => {
15 | if (!v.mount) return;
16 | shell.openExternal(`file://${v.mount}`);
17 | };
18 | return (
19 |
20 |
21 |
22 | {list.length ? null :
}
23 | {list.map(media => (
24 |
25 | - {media.name}
26 | {media.volumes.map((v) => {
27 | const percent = 1 - ((v.free_bytes || 0) / v.size_bytes);
28 | const width = `${Math.floor(percent * 100)}%`;
29 | const meta = v.free ? `${v.free} free of ${v.size}` : v.size;
30 | return (
31 | -
32 |
open(v)}>
33 |
34 |
35 | open(v)}>
36 |
{v.name}
37 |
40 |
{meta}
41 |
42 | eject(v)}>
43 |
44 |
45 |
46 | );
47 | })}
48 |
49 | ))}
50 |
51 |
52 | );
53 | }
54 |
55 | List.propTypes = {
56 | list: PropTypes.array,
57 | dispatch: PropTypes.func.isRequired
58 | };
59 | List.defaultProps = {
60 | list: []
61 | };
62 |
63 | export default connect(
64 | mapState({ list: 'list' })
65 | )(List);
66 |
--------------------------------------------------------------------------------
/src/components/Mount.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | function Mount({ dispatch }) {
6 | const gotoList = () => dispatch({
7 | type: 'gotoPage',
8 | payload: 'list'
9 | });
10 | const mount = () => dispatch({
11 | type: 'mount'
12 | });
13 | return (
14 |
23 | );
24 | }
25 |
26 | Mount.propTypes = {
27 | dispatch: PropTypes.func.isRequired
28 | };
29 |
30 | export default connect()(Mount);
31 |
--------------------------------------------------------------------------------
/src/components/NoData.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | function NoData({ dispatch }) {
6 | const refresh = () => dispatch({
7 | type: 'getMedias'
8 | });
9 | return (
10 |
11 |
No flash disk
12 |
Refresh by clicking below button
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | NoData.propTypes = {
23 | dispatch: PropTypes.func.isRequired
24 | };
25 |
26 | export default connect()(NoData);
27 |
--------------------------------------------------------------------------------
/src/icons/app.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/app.icns
--------------------------------------------------------------------------------
/src/icons/icon.less:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "iconfont";
3 | src: url('iconfont.eot?t=1505517923011'); /* IE9*/
4 | src: url('iconfont.eot?t=1505517923011#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), url('iconfont.ttf?t=1505517923011') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
6 | url('iconfont.svg?t=1505517923011#iconfont') format('svg'); /* iOS 4.1- */
7 | }
8 | .icon {
9 | font-family: "iconfont" !important;
10 | font-style: normal;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
14 | .icon-close:before {
15 | content: "\e627";
16 | }
17 | .icon-refresh:before {
18 | content: "\e68a";
19 | }
20 | .icon-hardisk:before {
21 | content: "\e68d";
22 | }
23 | .icon-more:before {
24 | content: "\e609";
25 | }
26 | .icon-eject:before {
27 | content: "\e642";
28 | }
29 | .icon-rocket:before {
30 | content: "\e505";
31 | }
--------------------------------------------------------------------------------
/src/icons/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.eot
--------------------------------------------------------------------------------
/src/icons/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
52 |
--------------------------------------------------------------------------------
/src/icons/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.ttf
--------------------------------------------------------------------------------
/src/icons/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.woff
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { ipcRenderer } from 'electron';
5 | import yax from 'yax';
6 | import App from './components/App';
7 | import './index.less';
8 |
9 | const store = yax({
10 | state: {
11 | mode: 'dark',
12 | page: 'list',
13 | list: []
14 | },
15 | reducers: {
16 | gotoPage(state, page) {
17 | return { ...state, page };
18 | },
19 | getMediasDone(state, list) {
20 | let page = 'list';
21 | // If has read-only goto mount page
22 | list.forEach((device) => {
23 | device.volumes.forEach((item) => {
24 | if (!item.writable) {
25 | page = 'mount';
26 | }
27 | });
28 | });
29 | return { ...state, list, page };
30 | },
31 | toggleMode(state) {
32 | const { mode } = state;
33 | return { ...state, mode: mode === 'dark' ? 'light' : 'dark' };
34 | }
35 | },
36 | actions: {
37 | mount() {
38 | ipcRenderer.send('mount-ntfs');
39 | },
40 | eject(ctx, data) {
41 | ipcRenderer.send('eject', data);
42 | },
43 | getMedias() {
44 | ipcRenderer.send('get-medias');
45 | }
46 | }
47 | });
48 |
49 | ipcRenderer.on('mount-done', () => {
50 | ipcRenderer.send('get-medias');
51 | });
52 | ipcRenderer.on('get-medias-done', (event, data) => {
53 | store.dispatch({
54 | type: 'getMediasDone',
55 | payload: data
56 | });
57 | });
58 | ipcRenderer.on('toggle-mode', () => {
59 | store.dispatch({
60 | type: 'toggleMode'
61 | });
62 | });
63 |
64 | render(
65 |
66 |
67 | ,
68 | document.getElementById('root')
69 | );
70 |
--------------------------------------------------------------------------------
/src/index.less:
--------------------------------------------------------------------------------
1 | @import './icons/icon.less';
2 |
3 | body {
4 | font-size: 14px;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
6 | }
7 | * {
8 | box-sizing: border-box;
9 | border: none;
10 | margin: 0;
11 | padding: 0;
12 | outline: none !important;
13 | }
14 | a {
15 | cursor: pointer;
16 |
17 | &:hover {
18 | opacity: 0.7;
19 | }
20 | &:active {
21 | opacity: 1;
22 | }
23 | }
24 | .dark {
25 | color: #fff;
26 | background-color: #13161f;
27 |
28 | .header {
29 | background-color: #1e1e2a;
30 | }
31 | .main .list {
32 | dt {
33 | color: #7f818e;
34 | }
35 | dd {
36 | background-color: #1e1e2a;
37 | }
38 | }
39 | }
40 | .light {
41 | color: #333;
42 | background-color: #f1f1f1;
43 |
44 | .btn {
45 | color: #3777cf;
46 | }
47 | .header {
48 | background-color: #fff;
49 | }
50 | .main .list {
51 | dt {
52 | color: #888;
53 | }
54 | dd {
55 | background-color: #fff;
56 | }
57 | }
58 | }
59 | .mount-page, .list-page {
60 | height: 100vh;
61 | display: flex;
62 | flex-direction: column;
63 | }
64 | .mount-page {
65 | position: relative;
66 | align-items: center;
67 | justify-content: center;
68 |
69 | .btn-close {
70 | position: absolute;
71 | top: 10px;
72 | right: 10px;
73 | font-size: 24px;
74 | }
75 | .btn-activate {
76 | width: 100px;
77 | height: 100px;
78 | border-radius: 50%;
79 | display: flex;
80 | flex-direction: column;
81 | align-items: center;
82 | justify-content: center;
83 | color: #fff;
84 | background-color: #3777cf;
85 |
86 | i {
87 | font-size: 32px;
88 | }
89 | }
90 | }
91 | .no-data {
92 | margin-top: 50%;
93 | text-align: center;
94 | line-height: 1.7;
95 |
96 | .btn-refresh {
97 | font-size: 40px;
98 | }
99 | }
100 | .header {
101 | padding: 10px;
102 | display: flex;
103 | align-items: center;
104 | justify-content: center;
105 | -webkit-user-select: none;
106 | -webkit-app-region: drag;
107 |
108 | .left, .right {
109 | width: 20%;
110 | }
111 | .right {
112 | text-align: right;
113 | line-height: 1;
114 | font-size: 20px;
115 |
116 | .btn {
117 | margin-left: 10px;
118 | }
119 | }
120 | .left {
121 | display: flex;
122 | align-items: center;
123 |
124 | .btn-close {
125 | display: inline-block;
126 | color: #fc605c;
127 | background-color: #fc605c;
128 | width: 12px;
129 | height: 12px;
130 | font-size: 8px;
131 | text-align: center;
132 | border-radius: 50%;
133 | border: 1px solid #f34f4a;
134 |
135 | &:hover {
136 | color: #13161f;
137 | opacity: 1;
138 | }
139 | }
140 | }
141 | .title {
142 | flex: 1;
143 | font-size: 16px;
144 | text-align: center;
145 | }
146 | }
147 | .main {
148 | flex: 1;
149 | overflow-y: scroll;
150 | }
151 | .main .list {
152 | dt {
153 | padding: 3px 15px;
154 | }
155 | dd {
156 | padding: 10px;
157 | display: flex;
158 | align-items: center;
159 | cursor: pointer;
160 |
161 | &:hover {
162 | color: #fff;
163 | background-color: #3777cf;
164 | }
165 | &.disabled {
166 | color: darken(#fff, 40%);
167 | }
168 | .info {
169 | flex: 1;
170 | line-height: 1;
171 | padding: 0 10px;
172 |
173 | .title {
174 | margin-bottom: 4px;
175 | }
176 | .progress {
177 | height: 8px;
178 | margin-bottom: 4px;
179 | border-radius: 2px;
180 | background-color: #1bc98e;
181 |
182 | .percent {
183 | width: 80%;
184 | height: 8px;
185 | border-radius: 2px;
186 | background-color: #1ca8dd;
187 | }
188 | }
189 | .meta {
190 | font-size: 12px;
191 | }
192 | }
193 | .left, .right {
194 | align-items: center;
195 | }
196 | .left i {
197 | font-size: 42px;
198 | }
199 | .right i {
200 | font-size: 18px;
201 | }
202 | }
203 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain } from 'electron';
2 | import getMediaList, { execAsync } from 'ls-usb';
3 | import sudo from 'sudo-prompt';
4 | import createMenu from './menu';
5 |
6 | function sudoAsync(cmd) {
7 | return new Promise((resolve, reject) => {
8 | sudo.exec(cmd, {
9 | name: 'Disky'
10 | }, (err) => {
11 | if (err) return reject(err);
12 | return resolve();
13 | });
14 | });
15 | }
16 |
17 | let win = null;
18 | app.on('ready', () => {
19 | win = new BrowserWindow({
20 | show: false,
21 | frame: false,
22 | width: 360,
23 | height: 572,
24 | fullscreenable: false,
25 | maximizable: false,
26 | resizable: false,
27 | backgroundColor: '#13161f'
28 | });
29 |
30 | win.once('ready-to-show', () => {
31 | win.show();
32 | });
33 |
34 | win.loadURL(`file://${__dirname}/app.html`);
35 |
36 | win.on('closed', () => {
37 | win = null;
38 | });
39 | ipcMain.on('open-menu', async (event, data) => {
40 | const menu = await createMenu(app, win, data);
41 | menu.popup();
42 | });
43 | });
44 |
45 | ipcMain.on('get-medias', async (event) => {
46 | const data = await getMediaList();
47 | event.sender.send('get-medias-done', data);
48 | });
49 | ipcMain.on('eject', async (event, arg) => {
50 | await execAsync(`diskutil eject ${arg}`);
51 | event.sender.send('mount-done');
52 | });
53 | ipcMain.on('mount-ntfs', async (event) => {
54 | const data = await getMediaList();
55 | const cmd = [];
56 | let isSudo = false;
57 | data.forEach((media) => {
58 | media.volumes.forEach((item) => {
59 | const { node, mount, fs_type, writable, udid } = item;
60 | if (!node || writable) return;
61 | if (fs_type === 'ntfs') {
62 | isSudo = true;
63 | const dir = mount || `/Volumes/${udid}`;
64 | if (mount) {
65 | cmd.push(`diskutil umount ${node}`);
66 | }
67 | cmd.push(`mkdir "${dir}"`);
68 | cmd.push(`mount -o rw,auto,nobrowse -t ntfs ${node} "${dir}"`);
69 | } else {
70 | cmd.push(`diskutil mount ${node}`);
71 | }
72 | });
73 | });
74 | if (cmd.length) {
75 | if (isSudo) {
76 | await sudoAsync(cmd.join('&&'));
77 | } else {
78 | await execAsync(cmd.join('&&'));
79 | }
80 | event.sender.send('mount-done');
81 | }
82 | });
83 |
--------------------------------------------------------------------------------
/src/menu.js:
--------------------------------------------------------------------------------
1 | import { Menu } from 'electron';
2 |
3 | export default function createMenu(app, win, mode) {
4 | return Menu.buildFromTemplate([{
5 | label: 'Dark mode',
6 | type: 'checkbox',
7 | checked: mode === 'dark',
8 | click() {
9 | win.webContents.send('toggle-mode');
10 | }
11 | }, {
12 | type: 'separator'
13 | }, {
14 | label: 'Quit',
15 | click: app.quit,
16 | role: 'quit'
17 | }]);
18 | }
19 |
--------------------------------------------------------------------------------