Compare commits

..

No commits in common. "master" and "v9.6.0" have entirely different histories.

357 changed files with 32026 additions and 21861 deletions

17
.appveyor.yml Normal file
View file

@ -0,0 +1,17 @@
platform: x64
install:
- ps: Install-Product node $env:nodejs_version $env:platform
build: off
build_script:
- node --version
- npm --version
- npm i now nupdate -g
- nupdate -a gritty
- now rm cloudcmd -y -t %NOW_TOKEN%
- now --public -t %NOW_TOKEN%
- now alias -t %NOW_TOKEN%

10
.babelrc Normal file
View file

@ -0,0 +1,10 @@
{
"presets": [
"env"
],
"plugins": [
"transform-object-assign",
"transform-object-rest-spread"
]
}

View file

@ -1,9 +0,0 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"module:babel-plugin-macros",
"@babel/plugin-transform-optional-chaining"
]
}

3
.bowerrc Normal file
View file

@ -0,0 +1,3 @@
{
"directory" : "modules"
}

View file

@ -1,4 +0,0 @@
last 2 Chrome versions
last 2 Safari versions
Firefox ESR
not dead

View file

@ -1,31 +0,0 @@
export default {
'F2 - Rename File': renameCurrent,
'L - Lint': run('npm run lint'),
'F - Fix Lint': run('npm run fix:lint'),
'T - Test': run('npm run test'),
'C - Coverage': run('npm run coverage'),
'D - Build Dev': run('npm run build:client:dev'),
'P - Build Prod': run('npm run build:client'),
};
async function renameCurrent(DOM) {
await DOM.renameCurrent();
}
function run(command) {
return async ({CloudCmd, DOM}) => {
const {TerminalRun, config} = CloudCmd;
const {CurrentInfo} = DOM;
const {dirPath} = CurrentInfo;
const cwd = config('root') + dirPath;
return await TerminalRun.show({
cwd,
command,
closeMessage: 'Press any key to close Terminal',
autoClose: false,
});
};
}

View file

@ -11,13 +11,27 @@ yarn-error.log
yarn.lock yarn.lock
now.json now.json
docker Dockerfile
Dockerfile.alpine
webpack.config.js webpack.config.js
cssnano.config.js
bin/release.js bin/release.js
modules/jquery/dist
modules/jquery/external
modules/jquery/src
!modules/jquery/dist/jquery.min.js
modules/fancybox/lib/
modules/fancybox/demo
modules/fancybox/sprite.psd
modules/execon
modules/emitify
client client
legacy
server_ server_

View file

@ -9,7 +9,7 @@ root = true
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = false trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4

22
.eslintrc Normal file
View file

@ -0,0 +1,22 @@
{
"env": {
"es6": true,
"node": true,
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"rules": {
"indent": ["error", 4],
"semi": "error",
"no-console": 0,
"no-use-before-define": ["error", "nofunc"]
},
"extends": [
"eslint:recommended"
]
}

15
.eslintrc.server Normal file
View file

@ -0,0 +1,15 @@
{
"env": {
"node": true
},
"rules": {
"no-process-exit": 0
},
"extends": [
".eslintrc",
"plugin:node/recommended"
],
"plugins": [
"node"
]
}

3
.github/FUNDING.yml vendored
View file

@ -1,3 +0,0 @@
github: coderaiser
open_collective: cloudcmd
ko_fi: coderaiser

12
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,12 @@
<!--
Thank you for reporting an issue. Please fill in the template below. If unsure
about something, just do as best as you're able.
-->
* **Version** (`cloudcmd -v`):
* **Node Version** `node -v`:
* **OS** (`uname -a` on Linux):
* **Browser name/version**:
* **Used Command Line Parameters**:
* **Changed Config**:

View file

@ -1,45 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: needs clarification
assignees: coderaiser
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- **Version** (`cloudcmd -v`):
- **Node Version** `node -v`:
- **OS** (`uname -a` on Linux):
- **Browser name/version**:
- **Used Command Line Parameters**:
- **Changed Config**:
```json
{}
```
- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd**
- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser**
- [ ] 💪 **I'm willing to work on this issue**
**Additional context**
Add any other context about the problem here.

View file

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Stack Overflow
url: https://stackoverflow.com/search?q=cloudcmd
about: Please ask and answer questions here.

View file

@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,24 +0,0 @@
***
name: Tracking issue
about: Create an issue with bug report or feature request.
title: ""
labels: needs triage
assignees: coderaiser
***
- **Version** (`cloudcmd -v`):
- **Node Version** `node -v`:
- **OS** (`uname -a` on Linux):
- **Browser name/version**:
- **Used Command Line Parameters**:
- **Changed Config**:
```json
{}
```
- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd**
- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser**
- [ ] 💪 **I'm willing to work on this issue**

View file

@ -4,5 +4,6 @@ about something, just do as best as you're able.
--> -->
- [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide") - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide")
- [ ] `npm run fix:lint` is OK - [ ] `npm run codestyle` is OK
- [ ] `npm test` is OK - [ ] `npm test` is OK

View file

@ -1,60 +0,0 @@
name: Docker IO
permissions:
contents: write
on:
- push
jobs:
buildx:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 24.x
- name: Install Redrun
run: bun i redrun -g --no-save
- name: NPM Install
run: bun i --no-save
- name: Lint
run: redrun lint
- name: Build
id: build
run: |
redrun build
VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push io-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.io
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:io
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io
ghcr.io/${{ github.repository }}-io
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io

View file

@ -1,88 +0,0 @@
name: Docker CI
permissions:
contents: write
on:
push:
tags:
- "*"
jobs:
buildx:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js 22.x
uses: actions/setup-node@v6
with:
node-version: 22.x
- name: Install Redrun
run: bun i redrun -g --no-save
- name: NPM Install
run: bun i --no-save
- name: Lint
run: redrun lint
- name: Build
id: build
run: |
redrun build
VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push base-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:latest
coderaiser/cloudcmd:${{ steps.build.outputs.version }}
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}
- name: Build and push alpine-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.alpine
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:alpine
coderaiser/cloudcmd:latest-alpine
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine
ghcr.io/${{ github.repository }}:latest-alpine
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine
- name: Build and push slim-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.slim
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:slim
coderaiser/cloudcmd:latest-slim
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-slim
ghcr.io/${{ github.repository }}-slim
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-slim

View file

@ -1,53 +0,0 @@
name: Node CI
on:
- push
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
env:
NAME: cloudcmd
strategy:
matrix:
node-version:
- 22.x
- 24.x
- 26.x
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Redrun
run: bun i redrun madrun -g --no-save
- name: Install
run: bun i --no-save
- name: Init Madrun
run: madrun --init
- name: Lint
run: redrun fix:lint
- name: Typos
uses: coderaiser/typos.ai@v1.1.8
with:
key: ${{ secrets.TYPOS_AI_KEY }}
- name: Commit fixes
uses: EndBug/add-and-commit@v10
continue-on-error: true
with:
message: "chore: ${{ env.NAME }}: actions: lint ☘️"
- name: Build
run: redrun build
- name: Test
run: redrun test
- name: Coverage
run: redrun coverage coverage:report
- name: Coveralls
uses: coverallsapp/github-action@v2
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

33
.gitignore vendored
View file

@ -1,17 +1,30 @@
*.swp
*.log
*.lock
.nyc_output
.DS_Store
.idea
package-lock.json package-lock.json
npm-debug.log* yarn.lock
yarn-error.log
node_modules node_modules
npm-debug.log*
coverage coverage
modules/jquery/dist
modules/jquery/external
modules/jquery/src
!modules/jquery/dist/jquery.min.js
modules/fancybox/lib/
modules/fancybox/demo
modules/fancybox/sprite.psd
modules/fancybox/*.json
modules/fancybox/gulpfile.js
modules/jquery-mousewheel
modules/execon modules/execon
modules/emitify modules/emitify
.nyc_output
*.swp
dist dist
dist-dev dist-dev

View file

@ -1,62 +0,0 @@
import {run, cutEnv} from 'madrun';
import {defineEnv} from 'supertape/env';
const testEnv = defineEnv({
timeout: 7000,
css: true,
});
const buildEnv = {
NODE_ENV: 'production',
};
export default {
'start': () => 'node bin/cloudcmd.js',
'start:dev': async () => await run('start', null, {
NODE_ENV: 'development',
}),
'build:start': () => run(['build:client', 'start']),
'build:start:dev': () => run([
'build:client:dev',
'start:dev',
]),
'lint:all': () => run('lint:progress'),
'lint': () => 'redlint fix; putout .',
'lint:progress': () => run('lint', '-f progress'),
'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"',
'fresh:lint': () => run('lint', '--fresh'),
'lint:fresh': () => run('lint', '--fresh'),
'fix:lint': async () => `putout --fix . && redlint fix`,
'lint:stream': () => run('lint', '-f stream'),
'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`],
'test:e2e': () => `tape 'test-e2e/**/*.js'`,
'test:client': () => `tape 'test/client/**/*.js'`,
'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`,
'wisdom': async () => await run(['lint:all', 'build', 'test'], null, {
CI: 1,
}),
'wisdom:type': () => 'bin/release.js',
'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`],
'coverage:report': () => 'c8 report --reporter=lcov',
'report': () => 'c8 report --reporter=lcov',
'6to5': () => [buildEnv, 'rspack build --config rspack.config.js'],
'6to5:client': () => run('6to5', '--mode production'),
'6to5:client:dev': async () => await run('6to5', '--mode development', {
NODE_ENV: 'development',
}),
'pre6to5:client': () => 'rimraf dist',
'pre6to5:client:dev': () => 'rimraf dist-dev',
'watch:client': () => run('6to5:client', '--watch'),
'watch:client:dev': () => run('6to5:client:dev', '--watch'),
'watch:server': () => 'nodemon bin/cloudcmd.js',
'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`],
'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`,
'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`,
'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`],
'watch:fix:lint': async () => `nodemon -w client -w server -w test -w common -x '${await run('fix:lint')}'`,
'build': async () => run('6to5:*'),
'build:dev': async () => run('build:client:dev'),
'build:client': () => run('6to5:client'),
'build:client:dev': () => run('6to5:client:dev'),
'heroku-postbuild': () => run('6to5:client'),
};

View file

@ -1,29 +1,33 @@
*.spec.*
*.config.*
*.fixture.js*
*.ai
*.cdr
*.eps
*.log
*.lock
.* .*
*.spec.js
manifest.yml manifest.yml
Dockerfile*
docker-compose.yml docker-compose.yml
now.json test
app.json
bower.json
deno.json
bin/release.*
img/logo/cloudcmd-hq.png
webpack.config.js
docker
test*
fixture
fixture-*
coverage coverage
css css
html html
yarn-error.log
yarn.lock
now.json
modules/jquery/dist
modules/jquery/external
modules/jquery/src
!modules/jquery/dist/jquery.min.js
modules/fancybox/lib/
modules/fancybox/demo
modules/fancybox/sprite.psd
app.json
bower.json
manifest.yml
bin/release.js
client client
webpack.config.js

View file

@ -1,15 +0,0 @@
{
"checkCoverage": false,
"all": false,
"exclude": [
"**/*.spec.js",
"**/*.*.js",
"**/*.config.*",
"**/fixture",
"**/test/**"
],
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100
}

View file

@ -1,60 +0,0 @@
{
"plugins": ["cloudcmd"],
"ignore": [
"*.md",
"app.json",
"fontello.json",
"html",
"fixture*"
],
"rules": {
"package-json/add-type": "off"
},
"match": {
".filesystem.json": {
"nodejs/rename-file-cjs-to-js": "off"
},
"base64": {
"types/convert-typeof-to-is-type": "off"
},
"*.md": {
"nodejs/convert-commonjs-to-esm": "on"
},
".webpack": {
"webpack": "on"
},
"server": {
"nodejs/remove-process-exit": "on"
},
"server/{server,exit}.js": {
"nodejs/remove-process-exit": "off"
},
"server/{server,exit,terminal,distribute/log}.{js,mjs}": {
"remove-console": "off"
},
"client/{client,cloudcmd,load-module}.{js,mjs}": {
"remove-console": "off"
},
"client": {
"nodejs": "off"
},
"client/sw": {
"remove-console": "off"
},
"test/common/cloudfunc.js": {
"remove-console": "off"
},
"storage.js": {
"promises/remove-useless-async": "off"
},
"docker.yml": {
"github/set-node-versions": "off"
},
"vim.js": {
"merge-duplicate-functions": "off"
},
"common": {
"nodejs/declare": "off"
}
}
}

View file

@ -1,38 +0,0 @@
import {env} from 'node:process';
import {rspack} from '@rspack/core';
const {CssExtractRspackPlugin} = rspack;
const isDev = env.NODE_ENV === 'development';
const plugins = [
new CssExtractRspackPlugin({
filename: '[name].css',
}),
];
const rules = [{
test: /\.css$/i,
use: [CssExtractRspackPlugin.loader, {
loader: 'css-loader',
options: {
url: true,
},
}],
}, {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
type: 'asset/inline',
}];
export default {
mode: isDev ? 'development' : 'production',
plugins,
module: {
rules,
},
optimization: {
minimize: !isDev,
minimizer: [
new rspack.LightningCssMinimizerRspackPlugin(),
],
},
};

View file

@ -1,36 +0,0 @@
import {env} from 'node:process';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const isDev = env.NODE_ENV === 'development';
export const plugins = [
new HtmlWebpackPlugin({
inject: false,
template: 'html/index.html',
minify: !isDev && getMinifyHtmlOptions(),
}),
];
function getMinifyHtmlOptions() {
return {
removeComments: true,
removeCommentsFromCDATA: true,
removeCDATASectionsFromCDATA: true,
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
/* оставляем, поскольку у нас
* в элемент fm генерируеться
* таблица файлов
*/
removeEmptyElements: false,
removeOptionalTags: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
};
}

View file

@ -1,173 +0,0 @@
import {resolve, sep} from 'node:path';
import {fileURLToPath} from 'node:url';
import {env} from 'node:process';
import {rspack} from '@rspack/core';
const resolveModule = (a) => fileURLToPath(import.meta.resolve(a));
const {
EnvironmentPlugin,
NormalModuleReplacementPlugin,
ProvidePlugin,
} = rspack;
const modules = './modules';
const dirModules = './client/modules';
const dirCss = './css';
const dirThemes = `${dirCss}/themes`;
const dirColumns = `${dirCss}/columns`;
const dir = './client';
const {NODE_ENV} = env;
const isDev = NODE_ENV === 'development';
const rootDir = new URL('..', import.meta.url).pathname;
const dist = resolve(rootDir, 'dist');
const distDev = resolve(rootDir, 'dist-dev');
const devtool = isDev ? 'eval' : 'source-map';
const noParse = (a) => a.endsWith('.spec.js');
// codegen.macro is a babel-macro (build-time codegen), not supported by
// Rspack's native SWC transform, so client/sw/sw.js (the only file that
// uses it) keeps going through babel-loader. Everything else uses
// Rspack's built-in SWC loader, which is the main source of the speedup.
const rules = [{
test: /sw\/sw\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.[mc]?js$/,
exclude: [/node_modules/, /sw\/sw\.js$/],
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'ecmascript',
},
},
env: {
targets: 'defaults',
},
},
}];
const plugins = [
new NormalModuleReplacementPlugin(/^node:/, (resource) => {
resource.request = resource.request.replace(/^node:/, '');
}),
new NormalModuleReplacementPlugin(/^putout$/, '@putout/bundle'),
new EnvironmentPlugin({
NODE_ENV,
}),
new ProvidePlugin({
process: 'process/browser',
}),
];
const splitChunks = {
chunks: 'all',
cacheGroups: {
abcCommon: {
name: 'cloudcmd.common',
chunks: (chunk) => {
const lazyChunks = [
'sw',
'nojs',
'view',
'edit',
'terminal',
'config',
'user-menu',
'help',
'themes/dark',
'themes/light',
'columns/name-size',
'columns/name-size-date',
'columns/name-size-time',
'columns/name-size-date-time',
];
return !lazyChunks.includes(chunk.name);
},
minChunks: 1,
enforce: true,
priority: -1,
reuseExistingChunk: true,
},
},
};
export default {
resolve: {
symlinks: false,
alias: {
'node:process': 'process',
'node:path': 'path',
},
fallback: {
path: resolveModule('path-browserify'),
process: resolveModule('process/browser'),
util: resolveModule('util'),
},
},
devtool,
optimization: {
splitChunks,
},
entry: {
'themes/dark': `${dirThemes}/dark.css`,
'themes/light': `${dirThemes}/light.css`,
'columns/name-size': `${dirColumns}/name-size.css`,
'columns/name-size-date': `${dirColumns}/name-size-date.css`,
'columns/name-size-date-time': `${dirColumns}/name-size-date-time.css`,
'nojs': `${dirCss}/nojs.css`,
'help': `${dirCss}/help.css`,
'view': `${dirCss}/view.css`,
'config': `${dirCss}/config.css`,
'terminal': `${dirCss}/terminal.css`,
'user-menu': `${dirCss}/user-menu.css`,
'sw': `${dir}/sw/sw.js`,
'cloudcmd': `${dir}/cloudcmd.js`,
[`${modules}/edit`]: `${dirModules}/edit.js`,
[`${modules}/edit-file`]: `${dirModules}/edit-file.js`,
[`${modules}/edit-file-vim`]: `${dirModules}/edit-file-vim.js`,
[`${modules}/edit-names`]: `${dirModules}/edit-names.js`,
[`${modules}/edit-names-vim`]: `${dirModules}/edit-names-vim.js`,
[`${modules}/menu`]: `${dirModules}/menu/index.js`,
[`${modules}/view`]: `${dirModules}/view/index.js`,
[`${modules}/help`]: `${dirModules}/help.js`,
[`${modules}/markdown`]: `${dirModules}/markdown.js`,
[`${modules}/config`]: `${dirModules}/config/index.js`,
[`${modules}/contact`]: `${dirModules}/contact.js`,
[`${modules}/upload`]: `${dirModules}/upload.js`,
[`${modules}/operation`]: `${dirModules}/operation/index.js`,
[`${modules}/konsole`]: `${dirModules}/konsole.js`,
[`${modules}/terminal`]: `${dirModules}/terminal.js`,
[`${modules}/terminal-run`]: `${dirModules}/terminal-run.js`,
[`${modules}/cloud`]: `${dirModules}/cloud.js`,
[`${modules}/user-menu`]: `${dirModules}/user-menu/index.js`,
[`${modules}/polyfill`]: `${dirModules}/polyfill.js`,
[`${modules}/command-line`]: `${dirModules}/command-line.js`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
module: {
rules,
noParse,
},
plugins,
performance: {
maxEntrypointSize: 800_000,
maxAssetSize: 600_000,
},
};
function devtoolModuleFilenameTemplate(info) {
const resource = info.absoluteResourcePath.replace(rootDir + sep, '');
return `file://cloudcmd/${resource}`;
}

16
.stylelintrc.yml Normal file
View file

@ -0,0 +1,16 @@
extends: stylelint-config-standard
rules:
indentation: 4
declaration-block-trailing-semicolon: always
declaration-colon-space-before: null
selector-list-comma-newline-after: null
comment-empty-line-before: null
number-leading-zero: null
number-no-trailing-zeros: null
string-quotes: single
function-url-quotes: never
no-eol-whitespace: null
font-family-name-quotes: always-unless-keyword
font-family-no-missing-generic-family-keyword: null
rule-empty-line-before: null

27
.travis.yml Normal file
View file

@ -0,0 +1,27 @@
language: node_js
node_js:
- 10
- 8
script:
- npm run lint && npm run build && npm run coverage && npm run report
notifications:
email:
on_success: never
on_failure: change
sudo: required
services:
- docker
deploy:
provider: script
script: npm run docker-ci
skip_cleanup: true
on:
node: '8'
tags: true
all_branches: false

View file

@ -1,2 +0,0 @@
[files]
extend-exclude = ["ChangeLog"]

72
.webpack/css.js Normal file
View file

@ -0,0 +1,72 @@
'use strict';
const fs = require('fs');
const {
basename,
extname,
join,
} = require('path');
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`);
const extractMain = extractCSS('[name]');
const cssNames = [
'nojs',
'view',
'config',
...getCSSList('columns'),
];
const cssPlugins = cssNames.map(extractCSS);
const plugins = [
...cssPlugins,
extractMain,
];
const rules = [{
test: /\.css$/,
exclude: /css\/(nojs|view|config|columns.*)\.css/,
use: extractMain.extract([
'css-loader?minimize',
]),
},
...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
loader: 'url-loader?limit=50000',
}];
module.exports = {
plugins,
module: {
rules,
},
};
function getCSSList(dir) {
const base = (a) => basename(a, extname(a));
const addDir = (name) => `${dir}/${name}`;
const rootDir = join(__dirname, '..');
return fs.readdirSync(`${rootDir}/css/${dir}`)
.map(base)
.map(addDir);
}
function extract(extractPlugin) {
const {filename} = extractPlugin;
const loader = isDev ? 'css-loader' : 'css-loader?minimize';
return {
test: RegExp(`css/${filename}`),
use: extractPlugin.extract([
loader
])
};
}

43
.webpack/html.js Normal file
View file

@ -0,0 +1,43 @@
'use strict';
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const plugins = [
new HtmlWebpackPlugin({
inject: false,
template: 'html/index.html',
minify: !isDev && getMinifyHtmlOptions(),
}),
];
module.exports = {
plugins,
};
function getMinifyHtmlOptions() {
return {
removeComments: true,
removeCommentsFromCDATA: true,
removeCDATASectionsFromCDATA: true,
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
/* оставляем, поскольку у нас
* в элемент fm генерируеться
* таблица файлов
*/
removeEmptyElements: false,
removeOptionalTags: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
};
}

93
.webpack/js.js Normal file
View file

@ -0,0 +1,93 @@
'use strict';
const {
resolve,
sep,
join,
} = require('path');
const dir = './client';
const dirModules = './client/modules';
const modules = './modules';
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const rootDir = join(__dirname, '..');
const dist = resolve(rootDir, 'dist');
const distDev = resolve(rootDir, 'dist-dev');
const devtool = isDev ? 'eval' : 'source-map';
const notEmpty = (a) => a;
const clean = (array) => array.filter(notEmpty);
const rules = clean([
!isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}]);
const splitChunks = {
chunks: 'all',
name: 'cloudcmd.common',
};
module.exports = {
devtool,
optimization: {
splitChunks,
},
entry: {
cloudcmd: `${dir}/cloudcmd.js`,
[modules + '/edit']: `${dirModules}/edit.js`,
[modules + '/edit-file']: `${dirModules}/edit-file.js`,
[modules + '/edit-file-vim']: `${dirModules}/edit-file-vim.js`,
[modules + '/edit-names']: `${dirModules}/edit-names.js`,
[modules + '/menu']: `${dirModules}/menu.js`,
[modules + '/view']: `${dirModules}/view.js`,
[modules + '/help']: `${dirModules}/help.js`,
[modules + '/markdown']: `${dirModules}/markdown.js`,
[modules + '/config']: `${dirModules}/config.js`,
[modules + '/contact']: `${dirModules}/contact.js`,
[modules + '/upload']: `${dirModules}/upload.js`,
[modules + '/operation']: `${dirModules}/operation/index.js`,
[modules + '/konsole']: `${dirModules}/konsole.js`,
[modules + '/terminal']: `${dirModules}/terminal.js`,
[modules + '/cloud']: `${dirModules}/cloud.js`,
[modules + '/polyfill']: `${dirModules}/polyfill.js`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
externals: [
externals
],
module: {
rules,
},
};
function externals(context, request, fn) {
if (!isDev)
return fn();
const list = [
'es6-promise',
];
if (list.includes(request))
return fn(null, request);
fn();
}
function devtoolModuleFilenameTemplate(info) {
const resource = info.absoluteResourcePath.replace(rootDir + sep, '');
return `file://cloudcmd/${resource}`;
}

View file

@ -10,7 +10,6 @@
".md" ".md"
], ],
"dictionary":[ "dictionary":[
"CloudCmd",
"Dev", "Dev",
"Dropbox", "Dropbox",
"Deepword", "Deepword",
@ -22,23 +21,17 @@
"Iptables", "Iptables",
"JitSu", "JitSu",
"Node", "Node",
"IO",
"Olena", "Olena",
"TarZak", "TarZak",
"Termux", "Termux",
"Zalitok", "Zalitok",
"WebSocket", "WebSocket",
"auth",
"autostart",
"binded",
"cd", "cd",
"cloudcmd", "cloudcmd",
"coderaiser", "coderaiser",
"com", "com",
"deepword",
"dev", "dev",
"destructuring", "deepword",
"dropbox",
"dword", "dword",
"edward", "edward",
"favicon", "favicon",
@ -46,18 +39,12 @@
"gz", "gz",
"io", "io",
"js", "js",
"linux",
"maintainers", "maintainers",
"markdown",
"microservice",
"minification", "minification",
"mouseup", "mouseup",
"named", "named",
"nginx", "nginx",
"npm",
"or io", "or io",
"patreon",
"rc",
"refactor", "refactor",
"sexualized", "sexualized",
"sslPort", "sslPort",
@ -66,7 +53,6 @@
"v0", "v0",
"v1", "v1",
"v2", "v2",
"yml", "yml"
"systemd"
] ]
} }

View file

@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include: Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language * Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences * Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism * Gracefully accepting constructive criticism
- Focusing on what is best for the community * Focusing on what is best for the community
- Showing empathy towards other community members * Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances * The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission * Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting * Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## Our Responsibilities

View file

@ -1,30 +1,27 @@
## Commit Commit
---------------
Format of the commit message: **type(scope) subject** Format of the commit message: **type(scope) subject**
**Type**: **Type**:
- feature(scope) subject
- feature: scope: subject - fix(scope) subject
- fix: scope: subject - docs(scope) subject
- docs: scope: subject - refactor(scope) subject
- refactor: scope: subject - test(scope) subject
- test: scope: subject - chore(scope) subject
- chore: scope: subject
**Scope**: **Scope**:
Scope could be anything specifying place of the commit change. Scope could be anything specifying place of the commit change.
For example util, console, view, edit, style etc... For example util, console, view, edit, style etc...
**Subject text**: **Subject text**:
- use imperative, present tense: “change” not “changed” nor “changes” - use imperative, present tense: “change” not “changed” nor “changes”
- don't capitalize first letter - don't capitalize first letter
- no dot (.) at the end - no dot (.) at the end
**Message body**: **Message body**:
- just as in <subject> use imperative, present tense: “change” not “changed” nor “changes” - just as in <subject> use imperative, present tense: “change” not “changed” nor “changes”
- includes motivation for the change and contrasts with previous behavior - includes motivation for the change and contrasts with previous behavior
**Examples**: **Examples**:
- [fix(style) .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050)
- [fix: style: .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) - [fix(console) dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec)
- [fix: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec)

2937
ChangeLog

File diff suppressed because it is too large Load diff

24
Dockerfile Normal file
View file

@ -0,0 +1,24 @@
FROM node:10.1
MAINTAINER Coderaiser
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm config set package-lock false && \
npm install --production && \
npm i gritty && \
npm cache clean --force
COPY . /usr/src/app
WORKDIR /root
ENV cloudcmd_terminal true
ENV cloudcmd_terminal_path gritty
EXPOSE 8000
ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"]

28
Dockerfile.alpine Normal file
View file

@ -0,0 +1,28 @@
FROM mhart/alpine-node:10.1
MAINTAINER Coderaiser
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm config set package-lock false && \
npm install --production && \
apk update && \
apk add --no-cache bash make g++ python && \
npm i gritty && \
npm cache clean --force && \
apk del make g++ python && \
rm -rf /usr/include /tmp/* /var/cache/apk/*
COPY . /usr/src/app
WORKDIR /root
ENV cloudcmd_terminal true
ENV cloudcmd_terminal_path gritty
EXPOSE 8000
ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"]

1449
HELP.md

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
(The MIT License) (The MIT License)
Copyright (c) 2012-2025 Coderaiser <mnemonic.enemy@gmail.com> Copyright (c) 2012-2018 Coderaiser <mnemonic.enemy@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

193
README.md
View file

@ -1,33 +1,45 @@
# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] # Cloud Commander v9.6.0 [![License][LicenseIMGURL]][LicenseURL] [![NPM version][NPMIMGURL]][NPMURL] [![Dependency Status][DependencyStatusIMGURL]][DependencyStatusURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Now status][BuildAppveyorIMGURL]][BuildAppveyorURL] [![Package Quality][PackageQualityIMGURL]][PackageQualityURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![OpenCollective](https://opencollective.com/cloudcmd/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/cloudcmd/sponsors/badge.svg)](#sponsors)
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] ### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL], [Now][NowURL])
[MainURL]: https://cloudcmd.io "Main" [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png
[BlogURL]: https://blog.cloudcmd.io "Blog" [MainURL]: http://cloudcmd.io "Main"
[SupportURL]: https://patreon.com/coderaiser "Patreon" [BlogURL]: http://blog.cloudcmd.io "Blog"
[DemoURL]: https://cloudcmd-zdp6.onrender.com [HerokuURL]: http://cloudcmd.herokuapp.com/ "Heroku"
[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png [NowURL]: https://cloudcmd.now.sh/ "Now"
[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" [NPMURL]: https://npmjs.org/package/cloudcmd "npm"
[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg [NPMIMGURL]: https://img.shields.io/npm/v/cloudcmd.svg?style=flat
[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd [LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License"
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 [LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat
[GitterURL]: https://gitter.im/cloudcmd/hello [DependencyStatusURL]: https://david-dm.org/coderaiser/cloudcmd
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg [DependencyStatusIMGURL]: https://img.shields.io/david/coderaiser/cloudcmd.svg?style=flat
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" [BuildStatusURL]: https://travis-ci.org/coderaiser/cloudcmd "Build Status"
[DeployIMG]: https://www.herokucdn.com/deploy/button.png [BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/cloudcmd.svg?style=flat
[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue
[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd
**Cloud Commander** a file manager for the web with console and editor. [BuildAppveyorURL]: https://ci.appveyor.com/project/coderaiser/cloudcmd
[BuildAppveyorIMGURL]: https://ci.appveyor.com/api/projects/status/tse6sc8dxrqxqehi?svg=true
![Cloud Commander](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander") [PackageQualityURL]: http://packagequality.com/#?package=cloudcmd "Package Quality"
[PackageQualityIMGURL]: http://packagequality.com/shield/cloudcmd.svg
[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36
[GitterURL]: https://gitter.im/cloudcmd/hello
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy"
[DeployIMG]: https://www.herokucdn.com/deploy/button.png
**Cloud Commander** is an orthodox web file manager with console and editor.
![Cloud Commander](http://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander")
## Install ## Install
``` ```
npm i cloudcmd -g npm i cloudcmd -g
``` ```
## Start ## Start
For starting just type in console: For starting just type in console:
@ -40,13 +52,7 @@ cloudcmd
Open url `http://localhost:8000` in browser. Open url `http://localhost:8000` in browser.
### View
You will see something similar to this.
![View](https://cloudcmd.io/img/screen/view.png "View")
## Deploy ## Deploy
`Cloud Commander` could be easily deployed to [Heroku][DeployURL]. `Cloud Commander` could be easily deployed to [Heroku][DeployURL].
[![Deploy][DeployIMG]][DeployURL] [![Deploy][DeployIMG]][DeployURL]
@ -70,31 +76,33 @@ npm i cloudcmd express socket.io -S
And create `index.js`: And create `index.js`:
```js ```js
import http from 'node:http'; const http = require('http');
import {cloudcmd} from 'cloudcmd'; const cloudcmd = require('cloudcmd');
import {Server} from 'socket.io'; const io = require('socket.io');
import express from 'express'; const app = require('express')();
const app = express();
const port = 1337; const port = 1337;
const prefix = '/'; const prefix = '/cloudcmd';
const server = http.createServer(app); const server = http.createServer(app);
const socket = new Server(server, { const socket = io.listen(server, {
path: `${prefix}socket.io`, path: `${prefix}/socket.io`
}); });
const config = { const config = {
name: 'cloudcmd :)', prefix // base URL or function which returns base URL (optional)
}; };
const plugins = [
__dirname + '/plugin.js'
];
const filePicker = { const filePicker = {
data: { data: {
FilePicker: { FilePicker: {
key: 'key', key: 'key',
}, }
}, }
}; };
// override option from json/modules.json // override option from json/modules.json
@ -102,40 +110,22 @@ const modules = {
filePicker, filePicker,
}; };
const { app.use(cloudcmd({
createConfigManager, socket, // used by Config, Edit (optional) and Console (required)
configPath, config, // config data (optional)
} = cloudcmd; plugins, // optional
const configManager = createConfigManager({
configPath,
});
app.use(prefix, cloudcmd({
socket, // used by Config, Edit (optional) and Console (required)
config, // config data (optional)
modules, // optional modules, // optional
configManager, // optional
})); }));
server.listen(port); server.listen(port);
``` ```
## Docker Docker
---------------
The docker images are provided for multiple architectures and types. The following list shows all existing images:
| Architecture | Type |
|----------------|--------------|
| amd64 | linux |
| arm64 (arm/v8) | linux |
| amd64 | linux-alpine |
| arm64 (arm/v8) | linux-alpine |
`Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way:
```sh ```sh
docker run -it --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd docker run -t --rm -v ~:/root -v /:/mnt/fs -p 8000:8000 coderaiser/cloudcmd
``` ```
Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`, Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`,
@ -161,19 +151,84 @@ When you create this file run:
docker-compose up docker-compose up
``` ```
## Documentation Get involved
---------------
More documentation you can find on https://cloudcmd.io/.
## Get involved
There is a lot ways to be involved in `Cloud Commander` development: There is a lot ways to be involved in `Cloud Commander` development:
- support project on patreon: https://patreon.com/coderaiser;
- if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); - if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue");
- if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); - if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request");
- if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); - if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki");
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/cloudcmd#backer)]
[![](https://opencollective.com/cloudcmd/backer/0/avatar.svg)](https://opencollective.com/cloudcmd/backer/0/website)
[![](https://opencollective.com/cloudcmd/backer/1/avatar.svg)](https://opencollective.com/cloudcmd/backer/1/website)
[![](https://opencollective.com/cloudcmd/backer/2/avatar.svg)](https://opencollective.com/cloudcmd/backer/2/website)
[![](https://opencollective.com/cloudcmd/backer/3/avatar.svg)](https://opencollective.com/cloudcmd/backer/3/website)
[![](https://opencollective.com/cloudcmd/backer/4/avatar.svg)](https://opencollective.com/cloudcmd/backer/4/website)
[![](https://opencollective.com/cloudcmd/backer/5/avatar.svg)](https://opencollective.com/cloudcmd/backer/5/website)
[![](https://opencollective.com/cloudcmd/backer/6/avatar.svg)](https://opencollective.com/cloudcmd/backer/6/website)
[![](https://opencollective.com/cloudcmd/backer/7/avatar.svg)](https://opencollective.com/cloudcmd/backer/7/website)
[![](https://opencollective.com/cloudcmd/backer/8/avatar.svg)](https://opencollective.com/cloudcmd/backer/8/website)
[![](https://opencollective.com/cloudcmd/backer/9/avatar.svg)](https://opencollective.com/cloudcmd/backer/9/website)
[![](https://opencollective.com/cloudcmd/backer/10/avatar.svg)](https://opencollective.com/cloudcmd/backer/10/website)
[![](https://opencollective.com/cloudcmd/backer/11/avatar.svg)](https://opencollective.com/cloudcmd/backer/11/website)
[![](https://opencollective.com/cloudcmd/backer/12/avatar.svg)](https://opencollective.com/cloudcmd/backer/12/website)
[![](https://opencollective.com/cloudcmd/backer/13/avatar.svg)](https://opencollective.com/cloudcmd/backer/13/website)
[![](https://opencollective.com/cloudcmd/backer/14/avatar.svg)](https://opencollective.com/cloudcmd/backer/14/website)
[![](https://opencollective.com/cloudcmd/backer/15/avatar.svg)](https://opencollective.com/cloudcmd/backer/15/website)
[![](https://opencollective.com/cloudcmd/backer/16/avatar.svg)](https://opencollective.com/cloudcmd/backer/16/website)
[![](https://opencollective.com/cloudcmd/backer/17/avatar.svg)](https://opencollective.com/cloudcmd/backer/17/website)
[![](https://opencollective.com/cloudcmd/backer/18/avatar.svg)](https://opencollective.com/cloudcmd/backer/18/website)
[![](https://opencollective.com/cloudcmd/backer/19/avatar.svg)](https://opencollective.com/cloudcmd/backer/19/website)
[![](https://opencollective.com/cloudcmd/backer/20/avatar.svg)](https://opencollective.com/cloudcmd/backer/20/website)
[![](https://opencollective.com/cloudcmd/backer/21/avatar.svg)](https://opencollective.com/cloudcmd/backer/21/website)
[![](https://opencollective.com/cloudcmd/backer/22/avatar.svg)](https://opencollective.com/cloudcmd/backer/22/website)
[![](https://opencollective.com/cloudcmd/backer/23/avatar.svg)](https://opencollective.com/cloudcmd/backer/23/website)
[![](https://opencollective.com/cloudcmd/backer/24/avatar.svg)](https://opencollective.com/cloudcmd/backer/24/website)
[![](https://opencollective.com/cloudcmd/backer/25/avatar.svg)](https://opencollective.com/cloudcmd/backer/25/website)
[![](https://opencollective.com/cloudcmd/backer/26/avatar.svg)](https://opencollective.com/cloudcmd/backer/26/website)
[![](https://opencollective.com/cloudcmd/backer/27/avatar.svg)](https://opencollective.com/cloudcmd/backer/27/website)
[![](https://opencollective.com/cloudcmd/backer/28/avatar.svg)](https://opencollective.com/cloudcmd/backer/28/website)
[![](https://opencollective.com/cloudcmd/backer/29/avatar.svg)](https://opencollective.com/cloudcmd/backer/29/website)
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/cloudcmd#sponsor)]
[![](https://opencollective.com/cloudcmd/sponsor/0/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/0/website)
[![](https://opencollective.com/cloudcmd/sponsor/1/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/1/website)
[![](https://opencollective.com/cloudcmd/sponsor/2/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/2/website)
[![](https://opencollective.com/cloudcmd/sponsor/3/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/3/website)
[![](https://opencollective.com/cloudcmd/sponsor/4/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/4/website)
[![](https://opencollective.com/cloudcmd/sponsor/5/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/5/website)
[![](https://opencollective.com/cloudcmd/sponsor/6/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/6/website)
[![](https://opencollective.com/cloudcmd/sponsor/7/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/7/website)
[![](https://opencollective.com/cloudcmd/sponsor/8/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/8/website)
[![](https://opencollective.com/cloudcmd/sponsor/9/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/9/website)
[![](https://opencollective.com/cloudcmd/sponsor/10/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/10/website)
[![](https://opencollective.com/cloudcmd/sponsor/11/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/11/website)
[![](https://opencollective.com/cloudcmd/sponsor/12/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/12/website)
[![](https://opencollective.com/cloudcmd/sponsor/13/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/13/website)
[![](https://opencollective.com/cloudcmd/sponsor/14/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/14/website)
[![](https://opencollective.com/cloudcmd/sponsor/15/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/15/website)
[![](https://opencollective.com/cloudcmd/sponsor/16/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/16/website)
[![](https://opencollective.com/cloudcmd/sponsor/17/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/17/website)
[![](https://opencollective.com/cloudcmd/sponsor/18/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/18/website)
[![](https://opencollective.com/cloudcmd/sponsor/19/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/19/website)
[![](https://opencollective.com/cloudcmd/sponsor/20/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/20/website)
[![](https://opencollective.com/cloudcmd/sponsor/21/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/21/website)
[![](https://opencollective.com/cloudcmd/sponsor/22/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/22/website)
[![](https://opencollective.com/cloudcmd/sponsor/23/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/23/website)
[![](https://opencollective.com/cloudcmd/sponsor/24/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/24/website)
[![](https://opencollective.com/cloudcmd/sponsor/25/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/25/website)
[![](https://opencollective.com/cloudcmd/sponsor/26/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/26/website)
[![](https://opencollective.com/cloudcmd/sponsor/27/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/27/website)
[![](https://opencollective.com/cloudcmd/sponsor/28/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/28/website)
[![](https://opencollective.com/cloudcmd/sponsor/29/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/29/website)
## License ## License
MIT MIT

View file

@ -11,7 +11,6 @@
"folder", "folder",
"orthodox" "orthodox"
], ],
"stack": "heroku-18",
"env": { "env": {
"NPM_CONFIG_PRODUCTION": { "NPM_CONFIG_PRODUCTION": {
"description": "Keep false to install devDependencies and build frontend", "description": "Keep false to install devDependencies and build frontend",
@ -33,11 +32,6 @@
"value": "false", "value": "false",
"required": false "required": false
}, },
"CLOUDCMD_CONFIG_AUTH": {
"description": "disable auth change in config dialog",
"value": "false",
"required": false
},
"CLOUDCMD_CONSOLE": { "CLOUDCMD_CONSOLE": {
"description": "enable console", "description": "enable console",
"value": "true", "value": "true",
@ -58,9 +52,9 @@
"value": "gritty", "value": "gritty",
"required": false "required": false
}, },
"CLOUDCMD_TERMINAL_COMMAND": { "CLOUDCMD_CONFIG_DIALOG": {
"description": "set command to run in terminal (shell by default)", "description": "show config dialog",
"value": "", "value": "false",
"required": false "required": false
}, },
"CLOUDCMD_ONE_FILE_PANEL": { "CLOUDCMD_ONE_FILE_PANEL": {
@ -98,6 +92,16 @@
"value": "", "value": "",
"required": false "required": false
}, },
"CLOUDCMD_ONE_PANEL_MODE": {
"description": "set one panel mode (deprecated use CLOUDCMD_ONE_FILE_PANEL)",
"value": "false",
"required": false
},
"CLOUDCMD_ONE_FILE_PANEL": {
"description": "set one panel mode",
"value": "false",
"required": false
},
"CLOUDCMD_VIM": { "CLOUDCMD_VIM": {
"description": "enable vim hot keys", "description": "enable vim hot keys",
"value": "false", "value": "false",
@ -108,11 +112,6 @@
"value": "name-size-date-owner-mode", "value": "name-size-date-owner-mode",
"required": false "required": false
}, },
"CLOUDCMD_OPEN": {
"description": "open web browser when server started",
"value": "false",
"required": false
},
"CLOUDCMD_CONFIRM_COPY": { "CLOUDCMD_CONFIRM_COPY": {
"description": "confirm copy", "description": "confirm copy",
"value": "true", "value": "true",
@ -123,39 +122,9 @@
"value": "true", "value": "true",
"required": false "required": false
}, },
"CLOUDCMD_SHOW_FILE_NAME": { "CLOUDCMD_CACHE": {
"description": "show file name in view and edit", "description": "enable cache",
"value": "false", "value": "true",
"required": false
},
"CLOUDCMD_EXPORT": {
"description": "enable export of config through a server",
"value": "false",
"required": false
},
"CLOUDCMD_EXPORT_TOKEN": {
"description": "authorization token used by export server",
"value": "root",
"required": false
},
"CLOUDCMD_IMPORT": {
"description": "enable import of config",
"value": "false",
"required": false
},
"CLOUDCMD_IMPORT_TOKEN": {
"description": "authorization token used to connect to export server",
"value": "root",
"required": false
},
"CLOUDCMD_IMPORT_URL": {
"description": "url of an import server",
"value": "http://localhost:8000",
"required": false
},
"CLOUDCMD_IMPORT_LISTEN": {
"description": "enable listen on config updates from import server",
"value": "false",
"required": false "required": false
} }
} }

View file

@ -1,48 +1,26 @@
#!/usr/bin/env node #!/usr/bin/env node
import process from 'node:process'; 'use strict';
import {promisify} from 'node:util';
import {tryToCatch} from 'try-to-catch';
import parse from 'yargs-parser';
import exit from '../server/exit.js';
import {createConfig, configPath} from '../server/config.js';
import * as env from '../server/env.js';
import prefixer from '../server/prefixer.js';
import * as validate from '../server/validate.js';
import Info from '../package.json' with {
type: 'json',
};
process.on('unhandledRejection', exit); const util = require('util');
const noop = () => {};
const isUndefined = (a) => typeof a === 'undefined'; const Info = require('../package');
const DIR_SERVER = '../server/';
const exit = require(DIR_SERVER + 'exit');
const config = require(DIR_SERVER + 'config');
const env = require(DIR_SERVER + 'env');
const choose = (a, b) => { const choose = (a, b) => {
if (isUndefined(a)) if (!a)
return b; return b;
return a; return a;
}; };
const config = createConfig({ const argv = process.argv;
configPath, const args = require('minimist')(argv.slice(2), {
});
const maybeRoot = (a) => {
if (a === '.')
return process.cwd();
return a;
};
const yargsOptions = {
configuration: {
'strip-aliased': true,
'strip-dashed': true,
},
coerce: {
root: maybeRoot,
},
string: [ string: [
'name', 'name',
'port', 'port',
@ -53,109 +31,77 @@ const yargsOptions = {
'packer', 'packer',
'root', 'root',
'prefix', 'prefix',
'prefix-socket',
'terminal-path', 'terminal-path',
'terminal-command',
'columns', 'columns',
'menu',
'theme',
'import-url',
'import-token',
'export-token',
'dropbox-token',
], ],
boolean: [ boolean: [
'auth', 'auth',
'cache',
'repl', 'repl',
'save', 'save',
'server', 'server',
'online', 'online',
'open', 'open',
'progress',
'config-dialog', 'config-dialog',
'config-auth',
'config-port',
'console', 'console',
'sync-console-path', 'sync-console-path',
'contact', 'contact',
'terminal', 'terminal',
'terminal-auto-restart',
'one-file-panel', 'one-file-panel',
'one-panel-mode',
'confirm-copy', 'confirm-copy',
'confirm-move', 'confirm-move',
'html-dialogs',
'show-config', 'show-config',
'show-dot-files',
'show-file-name',
'vim', 'vim',
'keys-panel', 'keys-panel',
'color',
'export',
'import',
'import-listen',
'log',
'zip',
'dropbox',
], ],
default: { default: {
'server': true, server : true,
'name': choose(env.parse('name'), config('name')), name : choose(env('name'), config('name')),
'auth': choose(env.bool('auth'), config('auth')), auth : choose(env('auth'), config('auth')),
'port': config('port'), cache : choose(env.bool('cache'), config('cache')),
'online': config('online'), port : config('port'),
'open': choose(env.bool('open'), config('open')), online : config('online'),
'editor': env.parse('editor') || config('editor'), open : config('open'),
'menu': env.parse('menu') || config('menu'), editor : env('editor') || config('editor'),
'packer': config('packer') || 'tar', packer : config('packer') || 'tar',
'zip': config('zip'), zip : config('zip'),
'username': env.parse('username') || config('username'), username : env('username') || config('username'),
'root': choose(env.parse('root'), config('root')), root : choose(env('root'), config('root')),
'prefix': choose(env.parse('prefix'), config('prefix')), prefix : config('prefix'),
'console': choose(env.bool('console'), config('console')), progress : config('progress'),
'contact': choose(env.bool('contact'), config('contact')), console : choose(env.bool('console'), config('console')),
'terminal': choose(env.bool('terminal'), config('terminal')), contact : choose(env.bool('contact'), config('contact')),
'columns': env.parse('columns') || config('columns') || '', terminal : choose(env.bool('terminal'), config('terminal')),
'theme': env.parse('theme') || config('theme') || '',
'vim': choose(env.bool('vim'), config('vim')),
'log': choose(env.bool('log'), config('log')),
'import-url': env.parse('import_url') || config('importUrl'),
'import-listen': choose(env.bool('import_listen'), config('importListen')),
'import': choose(env.bool('import'), config('import')),
'export': choose(env.bool('export'), config('export')),
'prefix-socket': config('prefixSocket'),
'show-dot-files': choose(env.bool('show_dot_files'), config('showDotFiles')),
'show-file-name': choose(env.bool('show_file_name'), config('showFileName')),
'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')),
'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')),
'config-auth': choose(env.bool('config_auth'), config('configAuth')), 'terminal-path': env('terminal_path') || config('terminalPath'),
'config-port': choose(env.bool('config_port'), config('configPort')), 'one-file-panel': choose(env.bool('one_file_panel'), config('onePanelMode')),
'terminal-path': env.parse('terminal_path') || config('terminalPath'), 'one-panel-mode': '',
'terminal-command': env.parse('terminal_command') || config('terminalCommand'),
'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')),
'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')),
'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')),
'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')),
'html-dialogs': config('htmlDialogs'),
'vim': choose(env.bool('vim'), config('vim')),
'columns': env('columns') || config('columns') || '',
'keys-panel': env.bool('keys_panel') || config('keysPanel'), 'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env.parse('import_token') || config('importToken'),
'export-token': env.parse('export_token') || config('exportToken'),
'dropbox': config('dropbox'),
'dropbox-token': config('dropboxToken') || '',
}, },
alias: { alias: {
version: 'v', v: 'version',
help: 'h', h: 'help',
password: 'p', p: 'password',
online: 'o', o: 'online',
username: 'u', u: 'username',
save: 's', s: 'save',
auth: 'a', a: 'auth',
config: 'c', c: 'config'
}, },
}; unknown: (cmd) => {
exit('\'%s\' is not a cloudcmd option. See \'cloudcmd --help\'.', cmd);
const {argv} = process; }
const args = parse(argv.slice(2), yargsOptions); });
if (args.version) if (args.version)
version(); version();
@ -164,112 +110,91 @@ else if (args.help)
else else
main(); main();
async function main() { function main() {
const {validateArgs} = await import('@putout/cli-validate-args');
const error = await validateArgs(args, [
...yargsOptions.boolean,
...yargsOptions.string,
]);
if (error)
return exit(error);
if (args.repl) if (args.repl)
await repl(); repl();
validate.columns(args.columns); checkUpdate();
validate.theme(args.theme);
port(args.port); port(args.port);
config('name', args.name); config('name', args.name);
config('auth', args.auth); config('auth', args.auth);
config('cache', args.cache);
config('online', args.online); config('online', args.online);
config('open', args.open); config('open', args.open);
config('username', args.username); config('username', args.username);
config('progress', args.progress);
config('console', args.console); config('console', args.console);
config('syncConsolePath', args.syncConsolePath); config('syncConsolePath', args['sync-console-path']);
config('showDotFiles', args.showDotFiles);
config('showFileName', args.showFileName);
config('contact', args.contact); config('contact', args.contact);
config('terminal', args.terminal); config('terminal', args.terminal);
config('terminalPath', args.terminalPath); config('terminalPath', args['terminal-path']);
config('terminalCommand', args.terminalCommand);
config('terminalAutoRestart', args.terminalAutoRestart);
config('editor', args.editor); config('editor', args.editor);
config('menu', args.menu); config('prefix', args.prefix);
config('prefix', prefixer(args.prefix)); config('root', args.root);
config('prefixSocket', prefixer(args.prefixSocket));
config('root', args.root || '/');
config('vim', args.vim); config('vim', args.vim);
config('theme', args.theme);
config('columns', args.columns); config('columns', args.columns);
config('log', args.log); config('htmlDialogs', args['html-dialogs']);
config('confirmCopy', args.confirmCopy); config('confirmCopy', args['confirm-copy']);
config('confirmMove', args.confirmMove); config('confirmMove', args['confirm-move']);
config('oneFilePanel', args.oneFilePanel); config('onePanelMode', args['one-file-panel']);
config('configDialog', args.configDialog); config('oneFilePanel', args['one-file-panel']);
config('configAuth', args.configAuth); config('configDialog', args['config-dialog']);
config('configPort', args.configPort); config('keysPanel', args['keys-panel']);
config('keysPanel', args.keysPanel);
config('export', args.export);
config('exportToken', args.exportToken);
config('import', args.import);
config('importToken', args.importToken);
config('importListen', args.importListen);
config('importUrl', args.importUrl);
config('dropbox', args.dropbox); if (args['one-panel-mode']) {
config('dropboxToken', args.dropboxToken); util.deprecate(noop, `cloudcmd --one-panel-mode: deprecated, use --one-file-panel instead`, 'DP0001')();
config('oneFilePanel', true);
config('onePanelMode', true);
} else if (typeof args['one-panel-mode'] === 'boolean') {
util.deprecate(noop, `cloudcmd --no-one-panel-mode: deprecated, use --no-one-file-panel instead`, 'DP0001')();
config('oneFilePanel', false);
config('onePanelMode', false);
}
await readConfig(args.config); if (env('one_panel_mode')) {
util.deprecate(noop, `CLOUDCMD_ONE_PANEL_MODE deprecated, use CLOUDCMD_ONE_FILE_PANEL instead`, 'DP0001')();
config('oneFilePanel', env.bool('one_panel_mode'));
config('onePanelMode', env.bool('one_panel_mode'));
}
readConfig(args.config);
const options = { const options = {
root: config('root'), root: args.root || '/', /* --no-root */
editor: config('editor'), editor: args.editor,
packer: config('packer'), packer: args.packer,
prefix: config('prefix'), prefix: args.prefix || '', /* --no-prefix */
prefixSocket: config('prefixSocket'), columns: args.columns,
columns: config('columns'),
theme: config('theme'),
menu: config('menu'),
}; };
const password = env.parse('password') || args.password; const password = env('password') || args.password;
if (password) if (password)
config('password', await getPassword(password)); config('password', getPassword(password));
validateRoot(options.root, config); validateRoot(options.root);
if (args.showConfig) if (args['show-config'])
await showConfig(); showConfig();
const {distributeImport} = await import('../server/distribute/import.js'); if (!args.save)
const importConfig = promisify(distributeImport); return start(options);
await start(options, config); config.save(() => {
start(options);
if (args.save) });
config.write();
await tryToCatch(checkUpdate);
await importConfig(config);
} }
function validateRoot(root, config) { function validateRoot(root) {
validate.root(root, config); const validate = require(DIR_SERVER + 'validate');
validate.root(root, console.log);
if (root === '/')
return;
if (config('log'))
console.log(`root: ${root}`);
} }
async function getPassword(password) { function getPassword(password) {
const {default: criton} = await import('criton'); const criton = require('criton');
return criton(password, config('algo')); return criton(password, config('algo'));
} }
@ -277,12 +202,11 @@ function version() {
console.log('v' + Info.version); console.log('v' + Info.version);
} }
async function start(options, config) { function start(config) {
if (!args.server) const SERVER = DIR_SERVER + 'server';
return;
const {default: server} = await import('../server/server.js'); if (args.server)
server(options, config); require(SERVER)(config);
} }
function port(arg) { function port(arg) {
@ -294,38 +218,40 @@ function port(arg) {
exit('cloudcmd --port: should be a number'); exit('cloudcmd --port: should be a number');
} }
async function showConfig() { function showConfig() {
const {showConfig} = await import('../server/show-config.js'); const show = require('../server/show-config');
const data = showConfig(config('*')); const data = show(config('*'));
console.log(data); console.log(data);
} }
async function readConfig(name) { function readConfig(name) {
if (!name) if (!name)
return; return;
const {default: forEachKey} = await import('for-each-key'); const fs = require('fs');
const tryCatch = require('try-catch');
const jju = require('jju');
const forEachKey = require('for-each-key');
const data = await import(name, { const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), {
with: { mode: 'json'
type: 'json',
},
}); });
const result = tryCatch(readjsonSync, name);
const error = result[0];
const data = result[1];
if (error)
return exit(error.message);
forEachKey(config, data); forEachKey(config, data);
} }
async function help() { function help() {
const {default: bin} = await import('../json/help.json', { const bin = require('../json/help');
with: { const forEachKey = require('for-each-key');
type: 'json', const currify = require('currify/legacy');
},
});
const {default: forEachKey} = await import('for-each-key');
const {default: currify} = await import('currify');
const usage = 'Usage: cloudcmd [options]'; const usage = 'Usage: cloudcmd [options]';
const url = Info.homepage; const url = Info.homepage;
const log = currify((a, b, c) => console.log(a, b, c)); const log = currify((a, b, c) => console.log(a, b, c));
@ -336,27 +262,36 @@ async function help() {
console.log('\nGeneral help using Cloud Commander: <%s>', url); console.log('\nGeneral help using Cloud Commander: <%s>', url);
} }
async function repl() { function repl() {
console.log('REPL mode enabled (telnet localhost 1337)'); console.log('REPL mode enabled (telnet localhost 1337)');
await import('../server/repl.js'); require(DIR_SERVER + 'repl');
} }
async function checkUpdate() { function checkUpdate() {
const {default: load} = await import('package-json'); const load = require('package-json');
const {version} = await load(Info.name, 'latest'); const noop = () => {};
await showUpdateInfo(version); load(Info.name, 'latest')
.then(showUpdateInfo)
.catch(noop);
} }
async function showUpdateInfo(version) { function showUpdateInfo(data) {
if (version === Info.version) const version = data.version;
return;
const {default: chalk} = await import('chalk'); if (version !== Info.version) {
const chalk = require('chalk');
const rendy = require('rendy');
const latestVersion = chalk.green.bold(`v${version}`); const latest = rendy('update available: {{ latest }}', {
const latest = `update available: ${latestVersion}`; latest: chalk.green.bold('v' + version),
const current = chalk.dim(`(current: v${Info.version})`); });
console.log('%s %s', latest, current); const current = chalk.dim(rendy('(current: v{{ current }})', {
current: Info.version
}));
console.log('%s %s', latest, current);
}
} }

View file

@ -1,26 +0,0 @@
import {spawnSync} from 'node:child_process';
import {test} from 'supertape';
import info from '../package.json' with {
type: 'json',
};
const cliPath = new URL('cloudcmd.js', import.meta.url).pathname;
test('cloudcmd: bin: cli: -h', (t) => {
const {stdout} = spawnSync(cliPath, ['-h'], {
encoding: 'utf8',
});
t.match(stdout, `Options`);
t.end();
});
test('cloudcmd: bin: cli: -v', (t) => {
const {version} = info;
const {stdout} = spawnSync(cliPath, ['-v'], {
encoding: 'utf8',
});
t.match(stdout, `v${version}`);
t.end();
});

View file

@ -1,72 +1,67 @@
#!/usr/bin/env node #!/usr/bin/env node
import {promisify} from 'node:util'; 'use strict';
import process from 'node:process';
import {tryToCatch} from 'try-to-catch';
import minor from 'minor';
import _place from 'place';
import {rendy} from 'rendy';
import shortdate from 'shortdate';
import Info from '../package.json' with {
type: 'json',
};
const place = promisify(_place); const DIR = '../';
const Info = require(DIR + 'package');
await main(); const minor = require('minor');
const place = require('place');
const rendy = require('rendy');
const shortdate = require('shortdate');
async function main() { const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major');
const history = '## Version history\n\n';
const link = '//github.com/coderaiser/cloudcmd/releases/tag/';
const template = '- ' +
'*{{ date }}*, ' +
'**[v{{ version }}]' +
'(' +
link +
'v{{ version }})**\n';
const {version} = Info;
const [error, versionNew] = await tryToCatch(cl);
main((error) => {
if (error) if (error)
return console.error(error); console.error(error.message);
});
await replaceVersion('README.md', version, versionNew); function main(callback) {
await replaceVersion('HELP.md', version, versionNew); const history = 'Version history\n---------------\n';
const link = '//github.com/coderaiser/cloudcmd/releases/tag/';
const template = '- *{{ date }}*, ' +
'**[v{{ version }}]' +
'(' + link + 'v{{ version }})**\n';
const historyNew = history + const version = Info.version;
rendy(template, {
date: shortdate(), cl((error, versionNew) => {
version: versionNew, if (error)
return callback(error);
replaceVersion('README.md', version, versionNew, callback);
replaceVersion('HELP.md', version, versionNew, function() {
const historyNew = history + rendy(template, {
date : shortdate(),
version : versionNew
});
replaceVersion('HELP.md', history, historyNew, callback);
}); });
});
await replaceVersion('HELP.md', history, historyNew);
} }
async function replaceVersion(name, version, versionNew) { function replaceVersion(name, version, versionNew, callback) {
const [error] = await tryToCatch(place, name, version, versionNew); place(name, version, versionNew, (error) => {
if (error)
return callback(error);
if (error) callback(null, 'done: ' + name);
return console.error(error); });
console.log(`done: ${name}`);
} }
async function cl() { function cl(callback) {
const {argv} = process; const argv = process.argv;
const length = argv.length - 1; const length = argv.length - 1;
const last = process.argv[length]; const last = process.argv[length];
const regExp = /^--(major|minor|patch)$/; const regExp = /^--(major|minor|patch)$/;
const [, match] = last.match(regExp) || []; const [, match] = last.match(regExp) || [];
console.log(last);
if (!regExp.test(last)) if (!regExp.test(last))
throw Error('ERROR: version is missing. release --patch|--minor|--major'); return callback(ERROR);
return getVersionNew(last, match); callback(null, getVersionNew(last, match));
} }
function getVersionNew(last, match) { function getVersionNew(last, match) {
@ -75,3 +70,4 @@ function getVersionNew(last, match) {
return last.substr(3); return last.substr(3);
} }

36
bower.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "cloudcmd",
"homepage": "https://github.com/coderaiser/cloudcmd",
"authors": [
"coderaiser <mnemonic.enemy@gmail.com>"
],
"description": "orthodox web file manager with console and editor",
"main": "bin/cloudcmd.js",
"keywords": [
"console",
"editor",
"file",
"file manager",
"folder",
"orthodox",
"view",
"copy",
"rename",
"move",
"delete",
"file operation"
],
"license": "MIT",
"private": true,
"ignore": [
"node_modules",
"modules",
"test"
],
"dependencies": {
"domtokenlist-shim": "~1.1.0",
"olark": "^1.0.0",
"jquery": "3.3.1",
"fancybox": "^2.1.6"
}
}

View file

@ -1,59 +1,64 @@
import process from 'node:process'; 'use strict';
import Emitify from 'emitify';
import inherits from 'inherits'; /* global Util, DOM */
import {rendy} from 'rendy';
import load from 'load.js'; const itype = require('itype/legacy');
import {tryToCatch} from 'try-to-catch'; const Emitify = require('emitify/legacy');
import {addSlashToEnd} from 'format-io'; const inherits = require('inherits');
import pascalCase from 'just-pascal-case'; const rendy = require('rendy/legacy');
import currify from 'currify'; const exec = require('execon');
import { const Images = require('./dom/images');
const join = require('join-io/www/join');
const jonny = require('jonny/legacy');
const currify = require('currify/legacy');
const bind = (f, ...a) => () => f(...a);
const noop = () => {};
const {
apiURL, apiURL,
formatMsg, formatMsg,
buildFromJSON, buildFromJSON,
} from '#common/cloudfunc'; } = require('../common/cloudfunc');
import * as Images from '#dom/images';
import {unregisterSW} from './sw/register.js';
import {getJsonFromFileTable} from './get-json-from-file-table.js';
import {Key} from './key/index.js';
import {loadModule} from './load-module.js';
const noJS = (a) => a.replace(/.js$/, ''); /* global Util, DOM */
const isDev = process.env.NODE_ENV === 'development';
inherits(CloudCmdProto, Emitify); inherits(CloudCmdProto, Emitify);
export const createCloudCmd = ({DOM, Listeners}) => { module.exports = new CloudCmdProto(Util, DOM);
return new CloudCmdProto({
DOM,
Listeners,
});
};
load.addErrorListener((e, src) => { function CloudCmdProto(Util, DOM) {
const msg = `file ${src} could not be loaded`; let Key;
Images.show.error(msg); let Debug;
}); let Listeners;
const log = (str) => {
if (!Debug)
return;
console.log(str);
};
function CloudCmdProto({DOM, Listeners}) {
Emitify.call(this); Emitify.call(this);
const CloudCmd = this; const CloudCmd = this;
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const Storage = DOM.Storage;
const Files = DOM.Files;
const {Storage, Files} = DOM; this.log = log;
this.PREFIX = '';
this.PREFIX_URL = '';
this.DIRCLIENT = '/dist/';
this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/';
this.log = () => { this.MIN_ONE_PANEL_WIDTH = 1155;
if (!isDev) this.HOST = location.origin ||
return; location.protocol + '//' + location.host;
};
this.prefix = ''; const TITLE = 'Cloud Commander';
this.prefixSocket = ''; this.TITLE = TITLE;
this.prefixURL = '';
this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width');
this.HOST = location.origin || location.protocol + '//' + location.host;
this.sort = { this.sort = {
left: 'name', left: 'name',
right: 'name', right: 'name',
@ -64,51 +69,143 @@ function CloudCmdProto({DOM, Listeners}) {
right: 'asc', right: 'asc',
}; };
this.changeDir = async (path, overrides = {}) => { log.enable = () => {
const { Debug = true;
isRefresh, };
panel,
history = true, log.disable = () => {
noCurrent, Debug = false;
currentName, };
} = overrides;
const kebabToCamelCase = Util.kebabToCamelCase;
/**
* Функция привязываеться ко всем ссылкам и
* загружает содержимое каталогов
*
* @param params - {
* paramLink - ссылка
* needRefresh - необходимость обязательной загрузки данных с сервера
* panel
* }
* @param callback
*/
this.loadDir = (params, callback) => {
const p = params;
const refresh = p.isRefresh;
const panel = p.panel;
const history = p.history;
const noCurrent = p.noCurrent;
const currentName = p.currentName;
const refresh = isRefresh;
let panelChanged; let panelChanged;
if (!noCurrent)
if (!noCurrent && panel && panel !== Info.panel) { if (panel && panel !== Info.panel) {
DOM.changePanel(); DOM.changePanel();
panelChanged = true; panelChanged = true;
} }
let imgPosition; let imgPosition;
if (panelChanged || refresh || !history) if (panelChanged || refresh || !history)
imgPosition = 'top'; imgPosition = 'top';
Images.show.load(imgPosition, panel); Images.show.load(imgPosition, panel);
/* загружаем содержимое каталога */ /* загружаем содержимое каталога */
await ajaxLoad(addSlashToEnd(path), { ajaxLoad(p.path, {
refresh, refresh,
history, history,
noCurrent, noCurrent,
currentName, currentName,
showDotFiles: CloudCmd.config('showDotFiles'), }, panel, callback);
}, panel);
}; };
/**
* function load modules
* @params = {name, path, func, dobefore, arg}
*/
function loadModule(params) {
if (!params)
return;
let path = params.path;
const name = params.name || path && kebabToCamelCase(path);
const func = params.func;
const funcName = params.funcName;
const doBefore = params.dobefore;
const isContain = /\.js/.test(path);
if (!isContain)
path += '.js';
if (CloudCmd[name])
return;
CloudCmd[name] = (...args) => {
const prefix = CloudCmd.PREFIX;
const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path;
exec(doBefore);
const done = (error) => {
const Proto = CloudCmd[name];
if (error || !itype.function(Proto))
return;
CloudCmd[name] = applyConstructor(Proto, args);
};
return DOM.load.js(pathFull, func || done);
};
CloudCmd[name][funcName] = CloudCmd[name];
}
/*
* apply arguemnts to constructor
*
* @param constructor
* @param args
*/
function applyConstructor(constructor, args) {
const F = function () {
return constructor.apply(this, args);
};
F.prototype = constructor.prototype;
return new F();
}
/** /**
* Конструктор CloudClient, который * Конструктор CloudClient, который
* выполняет весь функционал по * выполняет весь функционал по
* инициализации * инициализации
*/ */
this.init = async (prefix, config) => { this.init = (prefix, config) => {
CloudCmd.prefix = prefix; const func = bind(exec.parallel, [
CloudCmd.prefixURL = `${prefix}${apiURL}`; initModules,
CloudCmd.prefixSocket = config.prefixSocket; baseInit,
CloudCmd.DIR_DIST = `${prefix}/dist`; loadPlugins,
CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`; loadStyle,
exec.with(CloudCmd.route, location.hash),
], noop);
const funcBefore = (callback) => {
const src = prefix + '/join:' + [
CloudCmd.DIRCLIENT_MODULES + 'polyfill.js',
'/modules/domtokenlist-shim/dist/domtokenlist.min.js',
].join(':');
DOM.loadJquery(() => {
DOM.load.js(src, callback);
});
};
CloudCmd.PREFIX = prefix;
CloudCmd.PREFIX_URL = prefix + apiURL;
CloudCmd.config = (key) => config[key]; CloudCmd.config = (key) => config[key];
CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a)); CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a));
@ -117,22 +214,46 @@ function CloudCmdProto({DOM, Listeners}) {
* should be called from config.js only * should be called from config.js only
* after successful update on server * after successful update on server
*/ */
if (key === 'password') if (key === 'password')
return; return;
config[key] = value; config[key] = value;
}; };
if (config.oneFilePanel) DOM.Storage.setAllowed(CloudCmd.config('localStorage'));
if (config.onePanelMode)
CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity;
if (!document.body.scrollIntoViewIfNeeded) exec.if(document.body.scrollIntoViewIfNeeded, func, funcBefore);
await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`); };
await initModules(); function loadStyle(callback) {
await baseInit(); const prefix = CloudCmd.PREFIX;
const plugins = prefix + '/dist/cloudcmd.common.css';
CloudCmd.route(location.hash); DOM.load.css(plugins, callback);
}
function loadPlugins(callback) {
const prefix = CloudCmd.PREFIX;
const plugins = prefix + '/plugins.js';
DOM.load.js(plugins, callback);
}
this.join = (urls) => {
const prefix = CloudCmd.PREFIX;
if (!Array.isArray(urls))
throw Error('urls should be array!');
const noPrefixUrls = urls.map((url) => {
return url.replace(prefix, '');
});
return prefix + join(noPrefixUrls);
}; };
this.route = (path) => { this.route = (path) => {
@ -142,87 +263,91 @@ function CloudCmdProto({DOM, Listeners}) {
return; return;
const [kebabModule] = query; const [kebabModule] = query;
const module = noJS(pascalCase(kebabModule.slice(1))); const module = kebabToCamelCase(kebabModule.slice(1));
const [, file] = query; const file = query[1];
const current = DOM.getCurrentByName(file); const current = DOM.getCurrentByName(file);
if (file && !current) { if (file && !current) {
const msg = formatMsg('set current file', file, 'error'); const msg = formatMsg('set current file', file, 'error');
CloudCmd.log(msg); CloudCmd.log(msg);
return; return;
} }
DOM.setCurrentFile(current); DOM.setCurrentFile(current);
CloudCmd.execFromModule(module, 'show'); CloudCmd.execFromModule(module, 'show');
}; };
this.logOut = async () => { this.logOut = () => {
const url = CloudCmd.prefix + '/logout'; const url = CloudCmd.PREFIX + '/logout';
const error = () => document.location.reload(); const error = () => document.location.reload();
const {prefix} = CloudCmd;
await DOM.Storage.clear(); DOM.load.ajax({url, error});
unregisterSW(prefix);
DOM.load.ajax({
url,
error,
});
}; };
const initModules = async () => { function initModules(callback) {
CloudCmd.Key = Key; exec.if(CloudCmd.Key, () => {
CloudCmd.Key.bind(); Key = new CloudCmd.Key();
CloudCmd.Key = Key;
Key.bind();
}, (func) => {
/* привязываем клавиши к функциям */
const path = 'key.js';
const [, modules] = await tryToCatch(Files.get, 'modules');
const showLoad = Images.show.load;
const doBefore = {
edit: showLoad,
menu: showLoad,
};
const load = (name, path, dobefore) => {
loadModule({ loadModule({
name,
path, path,
dobefore, func
}); });
}; });
if (!modules) Files.get('modules', (error, modules) => {
return; const showLoad = Images.show.load;
for (const module of modules.local) { const doBefore = {
load(null, module, doBefore[module]); 'edit': showLoad,
} 'menu': showLoad,
}; };
async function saveCurrentName(currentName) { const load = (name, path, dobefore) => {
await Storage.set('current-name', currentName); const isTmpl = path === 'template';
const funcName = isTmpl ? 'get' : 'show';
loadModule({
name,
path,
dobefore,
funcName,
});
};
if (!modules)
modules = [];
modules.local.forEach((module) => {
load(null, module, doBefore[module]);
});
callback();
});
} }
async function baseInit() { function baseInit(callback) {
const files = DOM.getFiles(); const files = DOM.getFiles();
CloudCmd.on('current-file', DOM.updateCurrentInfo); CloudCmd.on('current-file', DOM.updateCurrentInfo);
CloudCmd.on('current-name', saveCurrentName);
const name = await Storage.get('current-name');
const currentFile = name && DOM.getCurrentByName(name) || files[0];
/* выделяем строку с первым файлом */ /* выделяем строку с первым файлом */
if (files) if (files)
DOM.setCurrentFile(currentFile, { DOM.setCurrentFile(files[0], {
// when hash is present // when hash is present
// it should be handled with this.route // it should be handled with this.route
// overwre otherwise // overwre otherwise
history: !location.hash, history: !location.hash
}); });
const dirPath = DOM.getCurrentDirPath(); const dirPath = DOM.getCurrentDirPath();
Listeners = CloudCmd.Listeners;
Listeners.init(); Listeners.init();
const panels = getPanels(); const panels = getPanels();
@ -231,18 +356,21 @@ function CloudCmdProto({DOM, Listeners}) {
Listeners.initKeysPanel(); Listeners.initKeysPanel();
if (!CloudCmd.config('dirStorage')) if (!CloudCmd.config('dirStorage'))
return; return callback();
const data = await Storage.get(dirPath); Storage.get(dirPath, (error, data) => {
if (!data) {
if (!data) data = getJSONfromFileTable();
await Storage.setJson(dirPath, getJsonFromFileTable()); Storage.set(dirPath, data);
}
callback();
});
} }
function getPanels() { function getPanels() {
const panels = ['left']; const panels = ['left'];
if (CloudCmd.config('oneFilePanel')) if (CloudCmd.config('onePanelMode'))
return panels; return panels;
return [ return [
@ -251,29 +379,40 @@ function CloudCmdProto({DOM, Listeners}) {
]; ];
} }
this.execFromModule = async (moduleName, funcName, ...args) => { this.execFromModule = (moduleName, funcName, ...args) => {
await CloudCmd[moduleName](); const obj = CloudCmd[moduleName];
const isObj = itype.object(obj);
const func = CloudCmd[moduleName][funcName]; exec.if(isObj, () => {
func(...args); const obj = CloudCmd[moduleName];
const func = obj[funcName];
func(...args);
}, obj);
}; };
this.refresh = async (options = {}) => { this.refresh = (options = {}, callback) => {
const {panel = Info.panel, currentName} = options; if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
const panel = options.panel || Info.panel;
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const isRefresh = true; const isRefresh = true;
const history = false; const history = false;
const noCurrent = options?.noCurrent; const noCurrent = options ? options.noCurrent : false;
const currentName = options.currentName;
await CloudCmd.changeDir(path, { CloudCmd.loadDir({
path,
isRefresh, isRefresh,
history, history,
panel, panel,
noCurrent, noCurrent,
currentName, currentName,
}); }, callback);
}; };
/** /**
@ -283,132 +422,176 @@ function CloudCmdProto({DOM, Listeners}) {
* @param options * @param options
* { refresh, history } - необходимость обновить данные о каталоге * { refresh, history } - необходимость обновить данные о каталоге
* @param panel * @param panel
* @param callback
* *
*/ */
async function ajaxLoad(path, options = {}, panel) { function ajaxLoad(path, options, panel, callback) {
const {RESTful} = DOM; const create = (error, json) => {
const RESTful = DOM.RESTful;
const name = options.currentName || Info.name;
const obj = jonny.parse(json);
const isRefresh = options.refresh;
const noCurrent = options.noCurrent;
CloudCmd.log(`reading dir: "${path}";`); if (!isRefresh && json)
return createFileTable(obj, panel, options, callback);
const dirStorage = CloudCmd.config('dirStorage'); const position = DOM.getPanelPosition(panel);
const json = dirStorage && await Storage.getJson(path); const sort = CloudCmd.sort[position];
const order = CloudCmd.order[position];
const name = options.currentName || Info.name; const query = rendy('?sort={{ sort }}&order={{ order }}', {
const {noCurrent, refresh} = options; sort,
order,
});
if (!refresh && json) RESTful.read(path + query, 'json', (error, obj) => {
return await createFileTable(json, panel, options); if (error)
return;
const position = DOM.getPanelPosition(panel); options.sort = sort;
const sort = CloudCmd.sort[position]; options.order = order;
const order = CloudCmd.order[position];
const query = rendy('?sort={{ sort }}&order={{ order }}', { createFileTable(obj, panel, options, () => {
sort, if (isRefresh && !noCurrent)
order, DOM.setCurrentByName(name);
});
const [, newObj] = await RESTful.read(path + query, 'json'); exec(callback);
});
if (!newObj) if (!CloudCmd.config('dirStorage'))
// that's OK, error handled by RESTful return;
return;
options.sort = sort; Storage.set(path, obj);
options.order = order; });
};
await createFileTable(newObj, panel, options); if (!options)
options = {};
if (refresh && !noCurrent) CloudCmd.log('reading dir: "' + path + '";');
DOM.setCurrentByName(name);
if (!CloudCmd.config('dirStorage')) if (!CloudCmd.config('dirStorage'))
return; return create();
Storage.setJson(path, newObj); Storage.get(path, create);
} }
/** /**
* Функция строит файловую таблицу * Функция строит файловую таблицу
* @param data - данные о файлах * @param json - данные о файлах
* @param panelParam * @param panelParam
* @param options - history, noCurrent, showDotFiles * @param history
* @param callback
*/ */
async function createFileTable(data, panelParam, options) { function createFileTable(json, panelParam, options, callback) {
const { const {
history, history,
noCurrent, noCurrent,
showDotFiles,
} = options; } = options;
const names = [ const names = ['file', 'path', 'link', 'pathLink'];
'file',
'path',
'link',
'pathLink',
];
const [error, [ Files.get(names, (error, templFile, templPath, templLink, templPathLink) => {
file, const Dialog = DOM.Dialog;
path, const panel = panelParam || DOM.getPanel();
link,
pathLink,
]] = await tryToCatch(Files.get, names);
if (error) const {
return DOM.Dialog.alert(error.responseText); dir,
name,
} = Info;
const panel = panelParam || DOM.getPanel(); if (error)
const {prefix} = CloudCmd; return Dialog.alert(TITLE, error.responseText);
const {dir, name} = Info; const childNodes = panel.childNodes;
let i = childNodes.length;
const {childNodes} = panel; while (i--)
let i = childNodes.length; panel.removeChild(panel.lastChild);
while (i--) panel.innerHTML = buildFromJSON({
panel.removeChild(panel.lastChild); sort : options.sort,
order : options.order,
panel.innerHTML = buildFromJSON({ data : json,
sort: options.sort, id : panel.id,
order: options.order, prefix : CloudCmd.PREFIX,
data, template : {
id: panel.id, file : templFile,
prefix, path : templPath,
showDotFiles, pathLink : templPathLink,
template: { link : templLink
file, }
path,
pathLink,
link,
},
});
Listeners.setOnPanel(panel);
if (!noCurrent) {
let current;
if (name === '..' && dir !== '/')
current = DOM.getCurrentByName(dir);
if (!current)
[current] = DOM.getFiles(panel);
DOM.setCurrentFile(current, {
history,
}); });
CloudCmd.emit('active-dir', Info.dirPath); Listeners.setOnPanel(panel);
}
if (!noCurrent) {
let current;
if (name === '..' && dir !== '/')
current = DOM.getCurrentByName(dir);
if (!current)
current = DOM.getFiles(panel)[0];
DOM.setCurrentFile(current, {
history: history
});
CloudCmd.emit('active-dir', Info.dirPath);
}
exec(callback);
});
} }
this.goToParentDir = async () => { /**
* Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень
*/
function getJSONfromFileTable() {
const path = DOM.getCurrentDirPath();
const infoFiles = Info.files || [];
const notParent = (current) => {
const name = DOM.getCurrentName(current);
return name !== '..';
};
const parse = (current) => {
const name = DOM.getCurrentName(current);
const size = DOM.getCurrentSize(current);
const owner = DOM.getCurrentOwner(current);
const mode = DOM.getCurrentMode(current);
const date = DOM.getCurrentDate(current);
return {
name,
size,
mode,
owner,
date,
};
};
const files = infoFiles
.filter(notParent)
.map(parse);
const fileTable = {
path,
files,
};
return fileTable;
}
this.goToParentDir = () => {
const dir = Info.dir;
const { const {
dir,
dirPath, dirPath,
parentDirPath, parentDirPath,
panel,
} = Info; } = Info;
if (dirPath === parentDirPath) if (dirPath === parentDirPath)
@ -416,13 +599,15 @@ function CloudCmdProto({DOM, Listeners}) {
const path = parentDirPath; const path = parentDirPath;
await CloudCmd.changeDir(path); CloudCmd.loadDir({path}, () => {
const {panel} = Info;
const current = DOM.getCurrentByName(dir);
const first = DOM.getFiles(panel)[0];
const current = DOM.getCurrentByName(dir); DOM.setCurrentFile(current || first, {
const [first] = DOM.getFiles(panel); history
});
DOM.setCurrentFile(current || first, {
history,
}); });
}; };
} }

View file

@ -1,44 +1,38 @@
import '../css/main.css'; 'use strict';
import process from 'node:process';
import wraptile from 'wraptile';
import load from 'load.js';
import * as Util from '#common/util';
import * as CloudFunc from '#common/cloudfunc';
import DOM from '#dom';
import {registerSW, listenSW} from './sw/register.js';
import {initSortPanel, sortPanel} from './sort.js';
import {createCloudCmd} from './client.js';
import * as Listeners from './listeners/index.js';
const isDev = process.env.NODE_ENV === 'development'; require('../css/main.css');
require('../css/nojs.css');
require('../css/columns/name-size-date.css');
require('../css/columns/name-size.css');
export default init; // prevent additional loading of emitify
window.Emitify = require('emitify/legacy');
globalThis.CloudCmd = init; module.exports = window.CloudCmd = (config) => {
window.Promise = window.Promise || require('es6-promise');
window.Util = require('../common/util');
window.CloudFunc = require('../common/cloudfunc');
async function init(config) { const DOM = require('./dom');
globalThis.CloudCmd = createCloudCmd({
DOM,
Listeners,
});
globalThis.DOM = DOM;
globalThis.Util = Util;
globalThis.CloudFunc = CloudFunc;
await register(config); window.DOM = DOM;
window.CloudCmd = require('./client');
const Dialog = require('./dom/dialog');
initSortPanel();
globalThis.CloudCmd.sortPanel = sortPanel;
const prefix = getPrefix(config.prefix); const prefix = getPrefix(config.prefix);
const {htmlDialogs} = config;
globalThis.CloudCmd.init(prefix, config); DOM.Dialog = Dialog(prefix, {
htmlDialogs
});
if (globalThis.CloudCmd.config('menu') === 'aleman') require('./listeners');
setTimeout(() => { require('./key');
import('https://esm.sh/@putout/processor-html'); require('./sort');
import('https://esm.sh/@putout/bundle@5.5');
}, 100); window.CloudCmd.init(prefix, config);
} };
function getPrefix(prefix) { function getPrefix(prefix) {
if (!prefix) if (!prefix)
@ -47,28 +41,6 @@ function getPrefix(prefix) {
if (!prefix.indexOf('/')) if (!prefix.indexOf('/'))
return prefix; return prefix;
return `/${prefix}`; return '/' + prefix;
} }
const onUpdateFound = wraptile(async (config) => {
if (isDev)
return;
const {DOM} = globalThis;
const prefix = getPrefix(config.prefix);
await load.js(`${prefix}/dist/cloudcmd.common.js`);
await load.js(`${prefix}/dist/cloudcmd.js`);
console.log('cloudcmd: sw: updated');
DOM.Events.removeAll();
globalThis.CloudCmd(config);
});
async function register(config) {
const {prefix} = config;
const sw = await registerSW(prefix);
listenSW(sw, 'updatefound', onUpdateFound(config));
}

View file

@ -1,124 +1,133 @@
/* global CloudCmd*/ 'use strict';
import * as Storage from '#dom/storage';
import tryToPromiseAll from '../../common/try-to-promise-all.js';
const CLASS = 'cut-file'; /* global CloudCmd */
const COPY = 'copy';
const CUT = 'cut';
function showMessage(msg) { const jonny = require('jonny/legacy');
globalThis.DOM.Dialog.alert(msg); const exec = require('execon');
}
function getNames() { const Storage = require('./storage');
const {DOM} = globalThis; const DOM = require('./');
const files = DOM.getActiveFiles();
return DOM.getFilenames(files); module.exports = new BufferProto();
}
function addCutClass() { function BufferProto() {
const {DOM} = globalThis; const Info = DOM.CurrentInfo;
const files = DOM.getActiveFiles(); const CLASS = 'cut-file';
const COPY = 'copy';
const CUT = 'cut';
const TITLE = 'Buffer';
for (const element of files) { const Buffer = {
element.classList.add(CLASS); cut : callIfEnabled.bind(null, cut),
copy : callIfEnabled.bind(null, copy),
clear : callIfEnabled.bind(null, clear),
paste : callIfEnabled.bind(null, paste)
};
function showMessage(msg) {
DOM.Dialog.alert(TITLE, msg);
} }
}
function rmCutClass() { function getNames() {
const {DOM} = globalThis; const files = DOM.getActiveFiles();
const files = DOM.getByClassAll(CLASS); const names = DOM.getFilenames(files);
for (const element of files) { return names;
element.classList.remove(CLASS);
} }
function addCutClass() {
const files = DOM.getActiveFiles();
files.forEach((element) => {
element.classList.add(CLASS);
});
}
function rmCutClass() {
const files = DOM.getByClassAll(CLASS);
[...files].forEach((element) => {
element.classList.remove(CLASS);
});
}
function callIfEnabled(callback) {
const is = CloudCmd.config('buffer');
if (is)
return callback();
showMessage('Buffer disabled in config!');
}
function copy() {
const names = getNames();
const from = Info.dirPath;
clear();
if (!names.length)
return;
Storage.remove(CUT)
.set(COPY, {
from,
names,
});
}
function cut() {
const names = getNames();
const from = Info.dirPath;
clear();
if (!names.length)
return;
addCutClass();
Storage
.set(CUT, {
from,
names,
});
}
function clear() {
Storage.remove(COPY)
.remove(CUT);
rmCutClass();
}
function paste() {
const copy = Storage.get.bind(Storage, COPY);
const cut = Storage.get.bind(Storage, CUT);
exec.parallel([copy, cut], (error, cp, ct) => {
const opStr = cp ? 'copy' : 'move';
const opData = cp || ct;
const Operation = CloudCmd.Operation;
const msg = 'Path is same!';
const path = Info.dirPath;
if (!error && !cp && !ct)
error = 'Buffer is empty!';
if (error)
return showMessage(error);
const data = jonny.parse(opData);
data.to = path;
if (data.from === path)
return showMessage(msg);
Operation.show(opStr, data);
clear();
});
}
return Buffer;
} }
const checkEnabled = (fn) => () => {
const is = CloudCmd.config('buffer');
if (is)
return fn();
showMessage('Buffer disabled in config!');
};
async function readBuffer() {
const [e, cp, ct] = await tryToPromiseAll([
Storage.getJson(COPY),
Storage.getJson(CUT),
]);
return [
e,
cp,
ct,
];
}
export const copy = checkEnabled(async () => {
const Info = globalThis.DOM.CurrentInfo;
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
await Storage.remove(CUT);
await Storage.setJson(COPY, {
from,
names,
});
});
export const cut = checkEnabled(async () => {
const Info = globalThis.DOM.CurrentInfo;
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
addCutClass();
await Storage.setJson(CUT, {
from,
names,
});
});
export const clear = checkEnabled(async () => {
await Storage.remove(COPY);
await Storage.remove(CUT);
rmCutClass();
});
export const paste = checkEnabled(async () => {
const Info = globalThis.DOM.CurrentInfo;
const [error, cp, ct] = await readBuffer();
if (error || !cp && !ct)
return showMessage(error || 'Buffer is empty!');
const opStr = cp ? 'copy' : 'move';
const data = cp || ct;
const {Operation} = CloudCmd;
const msg = 'Path is same!';
const to = Info.dirPath;
if (data.from === to)
return showMessage(msg);
Operation.show(opStr, {
...data,
to,
});
await clear();
});

View file

@ -1,83 +0,0 @@
/* global DOM */
const SELECTED_FILE = 'selected-file';
const Cmd = {
getSelectedFiles,
isSelected,
unselectFile,
selectFile,
selectAllFiles,
toggleSelectedFile,
toggleAllSelectedFiles,
};
/**
* selected file check
*
* @param currentFile
*/
export function isSelected(currentFile) {
if (!currentFile)
return false;
return DOM.isContainClass(currentFile, SELECTED_FILE);
}
/**
* select current file
* @param currentFile
*/
export function selectFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
current.classList.add(SELECTED_FILE);
return Cmd;
}
export function unselectFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
current.classList.remove(SELECTED_FILE);
return Cmd;
}
export function toggleSelectedFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
const name = DOM.getCurrentName(current);
if (name === '..')
return Cmd;
current.classList.toggle(SELECTED_FILE);
return Cmd;
}
export function toggleAllSelectedFiles() {
DOM
.getAllFiles()
.map(DOM.toggleSelectedFile);
return Cmd;
}
export function selectAllFiles() {
DOM
.getAllFiles()
.map(DOM.selectFile);
return Cmd;
}
/**
* unified way to get selected files
*
* @currentFile
*/
export function getSelectedFiles() {
const panel = DOM.getPanel();
const selected = DOM.getByClassAll(SELECTED_FILE, panel);
return Array.from(selected);
}

View file

@ -1,16 +1,25 @@
'use strict';
/* global DOM */ /* global DOM */
/* global CloudCmd */ /* global CloudCmd */
import createElement from '@cloudcmd/create-element';
import {getTitle, FS} from '#common/cloudfunc'; const btoa = require('../../common/btoa');
import {encode, decode} from '#common/entity';
const {
encode,
decode,
} = require('../../common/entity');
const {
getTitle,
FS,
} = require('../../common/cloudfunc');
let Title; let Title;
const CURRENT_FILE = 'current-file'; const CURRENT_FILE = 'current-file';
const encodeNBSP = (a) => a?.replace('\xa0', '&nbsp;'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const decodeNBSP = (a) => a?.replace('&nbsp;', '\xa0'); const SPACE = ' ';
export const _CURRENT_FILE = CURRENT_FILE;
/** /**
* set name from current (or param) file * set name from current (or param) file
@ -18,18 +27,18 @@ export const _CURRENT_FILE = CURRENT_FILE;
* @param name * @param name
* @param current * @param current
*/ */
export const setCurrentName = (name, current) => { module.exports.setCurrentName = (name, current) => {
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const {link} = Info; const {link} = Info;
const {prefix} = CloudCmd; const {PREFIX} = CloudCmd;
const dir = prefix + FS + Info.dirPath; const dir = PREFIX + FS + Info.dirPath;
const encoded = encode(name); const encoded = encode(name);
link.title = encoded; link.title = encoded;
link.href = dir + encoded; link.href = dir + encoded;
link.innerHTML = encoded; link.innerHTML = encoded;
current.setAttribute('data-name', createNameAttribute(name)); current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name)));
CloudCmd.emit('current-file', current); CloudCmd.emit('current-file', current);
return link; return link;
@ -40,48 +49,28 @@ export const setCurrentName = (name, current) => {
* *
* @param currentFile * @param currentFile
*/ */
export const getCurrentName = (currentFile) => { module.exports.getCurrentName = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
if (!current) if (!current)
return ''; return '';
return parseNameAttribute(current.getAttribute('data-name')); const link = DOM.getCurrentLink(current);
if (!link)
return '';
return decode(link.title)
.replace(NBSP_REG, SPACE);
}; };
/** /**
* Generate a `data-name` attribute for the given filename * get current direcotory path
* @param name The string name to encode
*/ */
const createNameAttribute = (name) => { module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => {
const encoded = btoa(encodeURI(name));
return `js-file-${encoded}`;
};
/**
* Parse a `data-name` attribute string back into the original filename
* @param attribute The string we wish to decode
*/
const parseNameAttribute = (attribute) => {
attribute = attribute.replace('js-file-', '');
return decodeNBSP(decodeURI(atob(attribute)));
};
export const _parseNameAttribute = parseNameAttribute;
const parseHrefAttribute = (prefix, attribute) => {
attribute = attribute.replace(RegExp('^' + prefix + FS), '');
return decode(decodeNBSP(attribute));
};
export const _parseHrefAttribute = parseHrefAttribute;
/**
* get current directory path
*/
export const getCurrentDirPath = (panel = DOM.getPanel()) => {
const path = DOM.getByDataName('js-path', panel); const path = DOM.getByDataName('js-path', panel);
return path.textContent; return path.textContent
.replace(NBSP_REG, SPACE);
}; };
/** /**
@ -89,31 +78,36 @@ export const getCurrentDirPath = (panel = DOM.getPanel()) => {
* *
* @param currentFile - current file by default * @param currentFile - current file by default
*/ */
export const getCurrentPath = (currentFile) => { module.exports.getCurrentPath = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const [element] = DOM.getByTag('a', current); const element = DOM.getByTag('a', current)[0];
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
return parseHrefAttribute(prefix, element.getAttribute('href')); const path = element
.getAttribute('href')
.replace(RegExp('^' + prefix + FS), '')
.replace(NBSP_REG, SPACE);
return decode(path);
}; };
/** /**
* get current directory name * get current direcotory name
*/ */
export const getCurrentDirName = () => { module.exports.getCurrentDirName = () => {
const href = DOM const href = DOM.getCurrentDirPath()
.getCurrentDirPath()
.replace(/\/$/, ''); .replace(/\/$/, '');
const substr = href.substr(href, href.lastIndexOf('/')); const substr = href.substr(href, href.lastIndexOf('/'));
const ret = href.replace(substr + '/', '') || '/';
return href.replace(`${substr}/`, '') || '/'; return ret;
}; };
/** /**
* get current directory path * get current direcotory path
*/ */
export const getParentDirPath = (panel) => { module.exports.getParentDirPath = (panel) => {
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const dirName = DOM.getCurrentDirName() + '/'; const dirName = DOM.getCurrentDirName() + '/';
const index = path.lastIndexOf(dirName); const index = path.lastIndexOf(dirName);
@ -125,11 +119,11 @@ export const getParentDirPath = (panel) => {
}; };
/** /**
* get not current directory path * get not current direcotory path
*/ */
export const getNotCurrentDirPath = () => { module.exports.getNotCurrentDirPath = () => {
const panel = DOM.getPanel({ const panel = DOM.getPanel({
active: false, active: false
}); });
return DOM.getCurrentDirPath(panel); return DOM.getCurrentDirPath(panel);
@ -140,20 +134,20 @@ export const getNotCurrentDirPath = () => {
* *
* @currentFile * @currentFile
*/ */
export const getCurrentFile = () => { module.exports.getCurrentFile = () => {
return DOM.getByClass(CURRENT_FILE); return DOM.getByClass(CURRENT_FILE);
}; };
/** /**
* get current file by name * get current file by name
*/ */
export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => {
const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); const dataName = 'js-file-' + btoa(encodeURI(name));
return DOM.getByDataName(dataName, panel); return DOM.getByDataName(dataName, panel);
}; };
/** /**
* private function that unset currentFile * private function thet unset currentfile
* *
* @currentFile * @currentFile
*/ */
@ -169,8 +163,9 @@ function unsetCurrentFile(currentFile) {
/** /**
* unified way to set current file * unified way to set current file
*/ */
export const setCurrentFile = (currentFile, options) => { module.exports.setCurrentFile = (currentFile, options) => {
const o = options; const o = options;
const CENTER = true;
const currentFileWas = DOM.getCurrentFile(); const currentFileWas = DOM.getCurrentFile();
if (!currentFile) if (!currentFile)
@ -198,15 +193,13 @@ export const setCurrentFile = (currentFile, options) => {
* but it should be false * but it should be false
* to prevent default behavior * to prevent default behavior
*/ */
if (!o || o.history) { if (!o || o.history !== false) {
const historyPath = path === '/' ? path : FS + path; const historyPath = path === '/' ? path : FS + path;
DOM.setHistory(historyPath, null, historyPath); DOM.setHistory(historyPath, null, historyPath);
} }
} }
/* scrolling to current file */ /* scrolling to current file */
const CENTER = true;
DOM.scrollIntoViewIfNeeded(currentFile, CENTER); DOM.scrollIntoViewIfNeeded(currentFile, CENTER);
CloudCmd.emit('current-file', currentFile); CloudCmd.emit('current-file', currentFile);
@ -216,18 +209,13 @@ export const setCurrentFile = (currentFile, options) => {
return DOM; return DOM;
}; };
export const setCurrentByName = (name) => {
const current = DOM.getCurrentByName(name);
return DOM.setCurrentFile(current);
};
/* /*
* set current file by position * set current file by position
* *
* @param layer - element * @param layer - element
* @param - position {x, y} * @param - position {x, y}
*/ */
export const getCurrentByPosition = ({x, y}) => { module.exports.getCurrentByPosition = ({x, y}) => {
const element = document.elementFromPoint(x, y); const element = document.elementFromPoint(x, y);
const getEl = (el) => { const getEl = (el) => {
@ -259,7 +247,7 @@ export const getCurrentByPosition = ({x, y}) => {
* *
* @param currentFile * @param currentFile
*/ */
export const isCurrentFile = (currentFile) => { module.exports.isCurrentFile = (currentFile) => {
if (!currentFile) if (!currentFile)
return false; return false;
@ -271,41 +259,18 @@ export const isCurrentFile = (currentFile) => {
* *
* @param name * @param name
*/ */
export const setTitle = (name) => {
module.exports.setTitle = (name) => {
if (!Title) if (!Title)
Title = DOM.getByTag('title')[0] || createElement('title', { Title = DOM.getByTag('title')[0] ||
innerHTML: name, DOM.load({
parent: document.head, name : 'title',
}); innerHTML : name,
parentElement : document.head
});
Title.textContent = name; Title.textContent = name;
return DOM; return DOM;
}; };
/**
* check is current file is a directory
*
* @param currentFile
*/
export const isCurrentIsDir = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const path = DOM.getCurrentPath(current);
const fileType = DOM.getCurrentType(current);
const isZip = path.endsWith('.zip');
const isDir = /^directory(-link)?/.test(fileType);
return isDir || isZip;
};
export const getCurrentType = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const el = DOM.getByDataName('js-type', current);
const type = el.className
.split(' ')
.pop();
return type;
};

View file

@ -1,114 +1,128 @@
import {test, stub} from 'supertape'; 'use strict';
import {create} from 'auto-globals';
import wraptile from 'wraptile';
import * as currentFile from './current-file.js';
const test = require('tape');
const diff = require('sinon-called-with-diff');
const sinon = diff(require('sinon'));
const id = (a) => a; const id = (a) => a;
const wraptile = require('wraptile');
const returns = wraptile(id); const returns = wraptile(id);
const {_CURRENT_FILE} = currentFile;
const currentFile = require('./current-file');
test('current-file: setCurrentName: setAttribute', (t) => { test('current-file: setCurrentName: setAttribute', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const setAttribute = sinon.stub();
const {setAttribute} = current; const current = {
setAttribute
};
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-aGVsbG8=',
], 'should call setAttribute');
t.end(); t.end();
}); });
test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { test('current-file: setCurrentName: setAttribute: cyrillic', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const setAttribute = sinon.stub();
const {setAttribute} = current; const current = {
setAttribute
};
currentFile.setCurrentName('ай', current); currentFile.setCurrentName('ай', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [ global.DOM = DOM;
'data-name', global.CloudCmd = CloudCmd;
'js-file-JUQwJUIwJUQwJUI5',
], 'should call setAttribute');
t.end();
});
test('current-file: getCurrentName', (t) => {
const current = create();
current.getAttribute.returns('js-file-Ymlu');
const result = currentFile.getCurrentName(current);
t.equal(result, 'bin');
t.end(); t.end();
}); });
test('current-file: emit', (t) => { test('current-file: emit', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const emit = stub(); const emit = sinon.stub();
const setAttribute = sinon.stub();
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd({ global.CloudCmd = getCloudCmd({
emit, emit,
}); });
const current = create(); const current = {
setAttribute,
};
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(emit.calledWith('current-file', current), 'should call emit');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(emit, ['current-file', current], 'should call emit');
t.end(); t.end();
}); });
test('current-file: setCurrentName: return', (t) => { test('current-file: setCurrentName: return', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const setAttribute = sinon.stub();
const link = {}; const link = {};
globalThis.DOM = getDOM({ global.DOM = getDOM({
link, link
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = {
setAttribute,
};
const current = create();
const result = currentFile.setCurrentName('hello', current); const result = currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.equal(result, link, 'should return link'); t.equal(result, link, 'should return link');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.end(); t.end();
}); });
test('current-file: getParentDirPath: result', (t) => { test('current-file: getParentDirPath: result', (t) => {
const {DOM} = globalThis; const {
DOM,
} = global;
const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirPath = returns('/D/Films/+++favorite films/');
const getCurrentDirName = returns('+++favorite films'); const getCurrentDirName = returns('+++favorite films');
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
}); });
@ -116,193 +130,33 @@ test('current-file: getParentDirPath: result', (t) => {
const result = currentFile.getParentDirPath(); const result = currentFile.getParentDirPath();
const expected = '/D/Films/'; const expected = '/D/Films/';
globalThis.DOM = DOM; global.DOM = DOM;
t.equal(result, expected, 'should return parent dir path'); t.equal(result, expected, 'should return parent dir path');
t.end(); t.end();
}); });
test('current-file: isCurrentFile: no', (t) => { function getCloudCmd({emit} = {}) {
const {DOM, CloudCmd} = globalThis; return {
PREFIX: '',
globalThis.DOM = getDOM(); emit: emit || sinon.stub(),
globalThis.CloudCmd = getCloudCmd(); };
}
const result = currentFile.isCurrentFile();
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.notOk(result);
t.end();
});
test('current-file: isCurrentFile', (t) => {
const {DOM, CloudCmd} = globalThis;
const isContainClass = stub();
globalThis.DOM = getDOM({
isContainClass,
});
globalThis.CloudCmd = getCloudCmd();
const current = {};
currentFile.isCurrentFile(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass');
t.end();
});
test('current-file: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {getByDataName} = globalThis.DOM;
getByDataName.returns({
className: 'mini-icon directory',
});
const current = create();
currentFile.getCurrentType(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(getByDataName, ['js-type', current]);
t.end();
});
test('current-file: isCurrentIsDir: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {getCurrentType} = globalThis.DOM;
const current = create();
currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(getCurrentType, [current]);
t.end();
});
test('current-file: isCurrentIsDir: directory', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('directory'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: directory-link', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('directory-link'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: file', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('file'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.notOk(result);
t.end();
});
const getCloudCmd = ({emit} = {}) => ({
prefix: '',
emit: emit || stub(),
});
test('current-file: parseNameAttribute', (t) => {
const result = currentFile._parseNameAttribute('js-file-aGVsbG8mbmJzcDt3b3JsZA==');
const expected = 'hello\xa0world';
t.equal(result, expected);
t.end();
});
test('current-file: parseHrefAttribute', (t) => {
const prefix = '/api/v1';
const result = currentFile._parseHrefAttribute(prefix, '/api/v1/fs/hello&nbsp;world');
const expected = '/hello\xa0world';
t.equal(result, expected);
t.end();
});
function getDOM(overrides = {}) {
const {
link = {},
getCurrentDirPath = stub(),
getCurrentDirName = stub(),
getByDataName = stub(),
isContainClass = stub(),
getCurrentType = stub(),
getCurrentPath = stub().returns(''),
} = overrides;
function getDOM({
link = {},
getCurrentDirPath = sinon.stub(),
getCurrentDirName = sinon.stub(),
getByDataName = sinon.stub(),
} = {}) {
return { return {
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
getCurrentPath,
getByDataName, getByDataName,
isContainClass,
getCurrentType,
CurrentInfo: { CurrentInfo: {
link, link,
dirPath: '/', dirPath: '/',
}, }
}; };
} }

View file

@ -1,18 +1,33 @@
import {tryToCatch} from 'try-to-catch'; 'use strict';
import * as smalltalk from 'smalltalk';
const title = 'Cloud Commander'; module.exports = Dialog;
export const alert = (...a) => smalltalk.alert(title, ...a, { const sm = require('smalltalk');
cancel: false, const smNative = require('smalltalk/native');
});
export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a); function Dialog(prefix, config) {
export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a); if (!(this instanceof Dialog))
export const progress = (...a) => smalltalk.progress(title, ...a); return new Dialog(prefix, config);
const {htmlDialogs} = config;
const smalltalk = htmlDialogs ? sm : smNative;
const alert = (title, message) => {
return smalltalk.alert(title, message);
};
this.alert = alert;
this.prompt = (title, message, value, options) => {
return smalltalk.prompt(title, message, value, options);
};
this.confirm = (title, message, options) => {
return smalltalk.confirm(title, message, options);
};
this.alert.noFiles = (title) => {
return alert(title, 'No files selected!');
};
}
alert.noFiles = () => {
return smalltalk.alert(title, 'No files selected!', {
cancel: false,
});
};

View file

@ -1,29 +1,33 @@
/* global DOM, CloudCmd */ /* global CloudCmd */
import philip from 'philip';
import * as Dialog from '#dom/dialog'; 'use strict';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images'; const philip = require('philip');
const Images = require('./images');
const {FS} = require('../../common/cloudfunc');
const DOM = require('.');
const {
getCurrentDirPath: getPathWhenRootEmpty,
} = DOM;
module.exports = (items) => {
const Dialog = DOM.Dialog;
export const uploadDirectory = (items) => {
if (items.length) if (items.length)
Images.show('top'); Images.show('top');
const entries = Array const entries = [...items].map((item) => {
.from(items) return item.webkitGetAsEntry();
.map((item) => item.webkitGetAsEntry());
const dirPath = DOM.getCurrentDirPath();
const path = dirPath.replace(/\/$/, '');
const progress = Dialog.progress('Uploading...');
progress.catch(() => {
Dialog.alert('Upload aborted');
uploader.abort();
}); });
const dirPath = getPathWhenRootEmpty();
const path = dirPath
.replace(/\/$/, '');
const uploader = philip(entries, (type, name, data, i, n, callback) => { const uploader = philip(entries, (type, name, data, i, n, callback) => {
const {prefixURL} = CloudCmd; const prefixURL = CloudCmd.PREFIX_URL;
const full = prefixURL + FS + path + name; const full = prefixURL + FS + path + name;
let upload; let upload;
@ -45,7 +49,7 @@ export const uploadDirectory = (items) => {
const max = next - current; const max = next - current;
const value = current + percent(count, 100, max); const value = current + percent(count, 100, max);
progress.setProgress(value); setProgress(value);
}); });
}); });
@ -54,11 +58,25 @@ export const uploadDirectory = (items) => {
uploader.abort(); uploader.abort();
}); });
uploader.on('progress', setProgress);
uploader.on('end', CloudCmd.refresh); uploader.on('end', CloudCmd.refresh);
}; };
const percent = (i, n, per = 100) => Math.round(i * per / n); function percent(i, n, per = 100) {
return Math.round(i * per / n);
}
const uploadFile = (url, data) => DOM.load.put(url, data); function setProgress(count) {
DOM.Images
.setProgress(count)
.show('top');
}
function uploadFile(url, data) {
return DOM.load.put(url, data);
}
function uploadDir(url) {
return DOM.load.put(url + '?dir');
}
const uploadDir = (url) => DOM.load.put(`${url}?dir`);

View file

@ -1,15 +1,6 @@
import currify from 'currify'; 'use strict';
const DOM = { const DOM = module.exports;
show,
hide,
getByClass,
getByClassAll,
getByDataName,
getById,
getByTag,
isContainClass,
};
/** /**
* check class of element * check class of element
@ -17,76 +8,71 @@ const DOM = {
* @param element * @param element
* @param className * @param className
*/ */
export function isContainClass(element, className) { module.exports.isContainClass = (element, className) => {
if (!element) if (!element)
throw Error('element could not be empty!'); throw Error('element could not be empty!');
if (!className) if (!className)
throw Error('className could not be empty!'); throw Error('className could not be empty!');
if (Array.isArray(className)) const classList = element.classList;
return className.some(currify( const ret = classList.contains(className);
isContainClass,
element,
));
const {classList} = element; return ret;
};
return classList.contains(className);
}
/** /**
* Function search element by tag * Function search element by tag
* @param tag - className * @param tag - className
* @param element - element * @param element - element
*/ */
export function getByTag(tag, element = document) { module.exports.getByTag = (tag, element = document) => {
return element.getElementsByTagName(tag); return element.getElementsByTagName(tag);
} };
/** /**
* Function search element by id * Function search element by id
* @param id * @param Id - id
* @param element
*/ */
export function getById(id, element = document) { module.exports.getById = (id, element = document) => {
return element.querySelector(`#${id}`); return element.querySelector('#' + id);
} };
/** /**
* Function search first element by class name * Function search first element by class name
* @param className - className * @param className - className
* @param element - element * @param element - element
*/ */
export function getByClass(className, element = document) { module.exports.getByClass = (className, element = document) => {
return DOM.getByClassAll(className, element)[0]; return DOM.getByClassAll(className, element)[0];
} };
export function getByDataName(attribute, element = document) { module.exports.getByDataName = (attribute, element = document) => {
const selector = '[' + 'data-name="' + attribute + '"]'; const selector = '[' + 'data-name="' + attribute + '"]';
return element.querySelector(selector); return element.querySelector(selector);
} };
/** /**
* Function search element by class name * Function search element by class name
* @param className * @param pClass - className
* @param element * @param element - element
*/ */
export function getByClassAll(className, element) { module.exports.getByClassAll = (className, element) => {
return (element || document).getElementsByClassName(className); return (element || document).getElementsByClassName(className);
} };
/** /**
* add class=hidden to element * add class=hidden to element
* *
* @param element * @param element
*/ */
export function hide(element) { module.exports.hide = (element) => {
element.classList.add('hidden'); element.classList.add('hidden');
return DOM; return DOM;
} };
export function show(element) { module.exports.show = (element) => {
element.classList.remove('hidden'); element.classList.remove('hidden');
return DOM; return DOM;
} };

View file

@ -1,152 +0,0 @@
import {test, stub} from 'supertape';
import {create} from 'auto-globals';
import {tryCatch} from 'try-catch';
import {
isContainClass,
getByTag,
getById,
getByClass,
getByDataName,
getByClassAll,
hide,
show,
} from './dom-tree.js';
test('dom: isContainClass: no element', (t) => {
const [e] = tryCatch(isContainClass);
t.equal(e.message, 'element could not be empty!', 'should throw when no element');
t.end();
});
test('dom: isContainClass: no className', (t) => {
const [e] = tryCatch(isContainClass, {});
t.equal(e.message, 'className could not be empty!', 'should throw when no element');
t.end();
});
test('dom: isContainClass: contains', (t) => {
const el = create();
const {contains} = el.classList;
const className = 'hello';
isContainClass(el, className);
t.calledWith(contains, [className], 'should call contains');
t.end();
});
test('dom: isContainClass: contains: array', (t) => {
const el = create();
const {contains} = el.classList;
const className = 'hello';
isContainClass(el, ['world', className, 'hello']);
t.calledWith(contains, [className], 'should call contains');
t.end();
});
test('dom: getByTag', (t) => {
const getElementsByTagName = stub();
const element = {
getElementsByTagName,
};
getByTag('div', element);
t.calledWith(getElementsByTagName, ['div'], 'should call getElementsByTagName');
t.end();
});
test('dom: getById', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getById('my-id', element);
t.calledWith(querySelector, ['#my-id'], 'should call querySelector with id selector');
t.end();
});
test('dom: getByClassAll', (t) => {
const getElementsByClassName = stub();
const element = {
getElementsByClassName,
};
getByClassAll('my-class', element);
t.calledWith(getElementsByClassName, ['my-class'], 'should call getElementsByClassName');
t.end();
});
test('dom: getByClass: calls getByClassAll', (t) => {
const element = {
getElementsByClassName: stub().returns(['first']),
};
const result = getByClass('my-class', element);
t.equal(result, 'first', 'should return first element from class list');
t.end();
});
test('dom: getByDataName', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getByDataName('hello', element);
t.calledWith(querySelector, ['[data-name="hello"]'], 'should call querySelector with data-name selector');
t.end();
});
test('dom: hide', (t) => {
const add = stub();
const element = {
classList: {
add,
},
};
hide(element);
t.calledWith(add, ['hidden'], 'should add hidden class');
t.end();
});
test('dom: show', (t) => {
const remove = stub();
const element = {
classList: {
remove,
},
};
show(element);
t.calledWith(remove, ['hidden'], 'should remove hidden class');
t.end();
});
test('dom: getByClassAll: without element uses document', (t) => {
const getElementsByClassName = stub();
const prevDocument = globalThis.document;
globalThis.document = {
getElementsByClassName,
};
getByClassAll('my-class');
globalThis.document = prevDocument;
t.calledWith(getElementsByClassName, ['my-class'], 'should fallback to document when no element');
t.end();
});

222
client/dom/events.js Normal file
View file

@ -0,0 +1,222 @@
'use strict';
const itype = require('itype/legacy');
module.exports = new EventsProto();
function EventsProto() {
const Events = this;
function parseArgs(eventName, element, listener, callback) {
let isFunc;
const args = [
eventName,
element,
listener,
callback,
];
const EVENT_NAME = 1;
const ELEMENT = 0;
const type = itype(eventName);
switch(type) {
default:
if (!/element$/.test(type))
throw Error('unknown eventName: ' + type);
parseArgs(
args[EVENT_NAME],
args[ELEMENT],
listener,
callback
);
break;
case 'string':
isFunc = itype.function(element);
if (isFunc) {
listener = element;
element = null;
}
if (!element)
element = window;
callback(element, [
eventName,
listener,
false
]);
break;
case 'array':
eventName.forEach((eventName) => {
parseArgs(
eventName,
element,
listener,
callback
);
});
break;
case 'object':
Object.keys(eventName).forEach((name) => {
const eventListener = eventName[name];
parseArgs(
name,
element,
eventListener,
callback
);
});
break;
}
}
/**
* safe add event listener
*
* @param type
* @param element {document by default}
* @param listener
*/
this.add = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
element.addEventListener.apply(element, args);
});
return Events;
};
/**
* safe add event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
};
if (!listener) {
listener = element;
element = null;
}
this.add(type, element, once);
return Events;
};
/**
* safe remove event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.remove = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
element.removeEventListener.apply(element, args);
});
return Events;
};
/**
* safe add event keydown listener
*
* @param listener
*/
this.addKey = function(...argsArr) {
const name = 'keydown';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe remove event click listener
*
* @param listener
*/
this.rmKey = function(...argsArr) {
const name = 'keydown';
const args = [name, ...argsArr];
return Events.remove(...args);
};
/**
* safe add event click listener
*
* @param listener
*/
this.addClick = function(...argsArr) {
const name = 'click';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe remove event click listener
*
* @param listener
*/
this.rmClick = function(...argsArr) {
const name = 'click';
const args = [name, ...argsArr];
return Events.remove(...args);
};
this.addContextMenu = function(...argsArr) {
const name = 'contextmenu';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe add event click listener
*
* @param listener
*/
this.addError = function(...argsArr) {
const name = 'error';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe add load click listener
*
* @param listener
*/
this.addLoad = function(...argsArr) {
const name = 'load';
const args = [name, ...argsArr];
return Events.add(...args);
};
function checkType(type) {
if (!type)
throw Error('type could not be empty!');
}
}

View file

@ -1,15 +0,0 @@
let list = [];
export const add = (el, name, fn) => {
list.push([
el,
name,
fn,
]);
};
export const clear = () => {
list = [];
};
export const get = () => list;

View file

@ -1,37 +0,0 @@
import {test} from 'supertape';
import * as eventStore from './event-store.js';
test('event-store: get', (t) => {
const el = {};
const name = 'click';
const fn = () => {};
eventStore.add(el, name, fn);
const result = eventStore.get();
const expected = [
[
el,
name,
fn,
],
];
t.deepEqual(result, expected);
t.end();
});
test('event-store: clear', (t) => {
const el = {};
const name = 'click';
const fn = () => {};
eventStore.add(el, name, fn);
eventStore.clear();
const result = eventStore.get();
const expected = [];
t.deepEqual(result, expected);
t.end();
});

View file

@ -1,203 +0,0 @@
import itype from 'itype';
import * as EventStore from './event-store.js';
/**
* safe add event listener
*
* @param type
* @param element - document by default
* @param listener
*/
export const add = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
const [name, fn, options] = args;
element.addEventListener(name, fn, options);
EventStore.add(element, name, fn);
});
return Events;
};
/**
* safe add event listener
*
* @param type
* @param listener
* @param element - document by default
*/
export const addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
};
if (!listener) {
listener = element;
element = null;
}
add(type, element, once);
return Events;
};
/**
* safe remove event listener
*
* @param type
* @param listener
* @param element - document by default
*/
export const remove = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
element.removeEventListener(...args);
});
return Events;
};
/**
* remove all added event listeners
*/
export const removeAll = () => {
const events = EventStore.get();
for (const [el, name, fn] of events)
el.removeEventListener(name, fn);
EventStore.clear();
};
/**
* safe add event keydown listener
*
* @param args
*/
export const addKey = function(...args) {
return add('keydown', ...args);
};
/**
* safe remove event click listener
*
* @param args
*/
export const rmKey = function(...args) {
return Events.remove('keydown', ...args);
};
/**
* safe add event click listener
*/
export const addClick = function(...args) {
return Events.add('click', ...args);
};
/**
* safe remove event click listener
*/
export const rmClick = function(...args) {
return remove('click', ...args);
};
export const addContextMenu = function(...args) {
return add('contextmenu', ...args);
};
/**
* safe add load listener
*/
export const addLoad = function(...args) {
return add('load', ...args);
};
function checkType(type) {
if (!type)
throw Error('type could not be empty!');
}
const getEventOptions = (eventName) => {
if (eventName !== 'touchstart')
return false;
return {
passive: true,
};
};
function parseArgs(eventName, element, listener, callback) {
let isFunc;
const args = [
eventName,
element,
listener,
callback,
];
const EVENT_NAME = 1;
const ELEMENT = 0;
const type = itype(eventName);
switch(type) {
default:
if (!type.endsWith('element'))
throw Error(`unknown eventName: ${type}`);
parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback);
break;
case 'string':
isFunc = itype.function(element);
if (isFunc) {
listener = element;
element = null;
}
if (!element)
element = window;
callback(element, [
eventName,
listener,
getEventOptions(eventName),
]);
break;
case 'array':
for (const name of eventName) {
parseArgs(name, element, listener, callback);
}
break;
case 'object':
for (const name of Object.keys(eventName)) {
const eventListener = eventName[name];
parseArgs(name, element, eventListener, callback);
}
break;
}
}
const Events = {
add,
addClick,
addContextMenu,
addKey,
addLoad,
addOnce,
remove,
removeAll,
rmClick,
rmKey,
};

View file

@ -1,82 +1,105 @@
/* global CloudCmd */ /* global CloudCmd */
import itype from 'itype';
import {promisify} from 'es6-promisify'; 'use strict';
import * as load from '#dom/load';
import * as RESTful from '#dom/rest'; const itype = require('itype/legacy');
const currify = require('currify/legacy');
const exec = require('execon');
const Storage = require('./storage');
const load = require('./load');
const RESTful = require('./rest');
const Promises = {}; const Promises = {};
const FILES_JSON = 'config|modules'; const FILES_JSON = 'config|modules';
const FILES_HTML = 'file|path|link|pathLink|media'; const FILES_HTML = 'file|path|link|pathLink|media';
const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload'; const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload';
const DIR_HTML = '/tmpl/'; const DIR_HTML = '/tmpl/';
const DIR_HTML_FS = `${DIR_HTML}fs/`; const DIR_HTML_FS = DIR_HTML + 'fs/';
const DIR_JSON = '/json/'; const DIR_JSON = '/json/';
const timeout = getTimeoutOnce(2000); const timeout = getTimeoutOnce(2000);
export const get = getFile; const get = currify(getFile);
const unaryMap = (array, fn) => array.map((a) => fn(a));
function getFile(name) { module.exports.get = get;
function getFile(name, callback) {
const type = itype(name); const type = itype(name);
check(name); let array;
if (type === 'string') check(name, callback);
return getModule(name);
if (type === 'array') switch(type) {
return Promise.all(name.map(getFile)); case 'string':
getModule(name, callback);
break;
case 'array':
array = unaryMap(name, get);
exec.parallel(array, callback);
break;
}
} }
function check(name) { function check(name, callback) {
if (!name) if (!name)
throw Error('name could not be empty!'); throw Error('name could not be empty!');
if (typeof callback !== 'function')
throw Error('callback should be a function');
} }
function getModule(name) { function getModule(name, callback) {
const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); let path;
const regExpJSON = RegExp(FILES_JSON);
const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT);
const regExpJSON = new RegExp(FILES_JSON);
const isHTML = regExpHTML.test(name); const isHTML = regExpHTML.test(name);
const isJSON = regExpJSON.test(name); const isJSON = regExpJSON.test(name);
if (!isHTML && !isJSON) if (!isHTML && !isJSON) {
return showError(name); showError(name);
} else if (name === 'config') {
getConfig(callback);
} else {
path = getPath(name, isHTML, isJSON);
if (name === 'config') getSystemFile(path, callback);
return getConfig(); }
const path = getPath(name, isHTML, isJSON);
return getSystemFile(path);
} }
function getPath(name, isHTML, isJSON) { function getPath(name, isHTML, isJSON) {
let path; let path;
const regExp = RegExp(FILES_HTML_ROOT); const regExp = new RegExp(FILES_HTML_ROOT);
const isRoot = regExp.test(name); const isRoot = regExp.test(name);
if (isHTML) { if (isHTML) {
if (isRoot) if (isRoot)
path = DIR_HTML + name.replace('-tmpl', ''); path = DIR_HTML + name.replace('-tmpl', '');
else else
path = DIR_HTML_FS + name; path = DIR_HTML_FS + name;
path += '.hbs'; path += '.hbs';
} else if (isJSON) { } else if (isJSON) {
path = DIR_JSON + name + '.json'; path = DIR_JSON + name + '.json';
} }
return path; return path;
} }
function showError(name) { function showError(name) {
const str = `Wrong file name: ${name}`; const str = 'Wrong file name: ' + name;
const error = Error(str); const error = new Error(str);
throw error; throw(error);
} }
const getSystemFile = promisify((file, callback) => { function getSystemFile(file, callback) {
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
if (!Promises[file]) if (!Promises[file])
Promises[file] = new Promise((success, error) => { Promises[file] = new Promise((success, error) => {
@ -85,7 +108,7 @@ const getSystemFile = promisify((file, callback) => {
load.ajax({ load.ajax({
url, url,
success, success,
error, error
}); });
}); });
@ -95,29 +118,37 @@ const getSystemFile = promisify((file, callback) => {
Promises[file] = null; Promises[file] = null;
callback(error); callback(error);
}); });
}); }
const getConfig = async () => { function getConfig(callback) {
let is; let is;
if (!Promises.config) if (!Promises.config)
Promises.config = () => { Promises.config = new Promise((resolve, reject) => {
is = true; is = true;
return RESTful.Config.read(); RESTful.Config.read((error, data) => {
}; if (error)
return reject(error);
const [, data] = await Promises.config(); resolve(data);
});
});
if (data) Promises.config.then((data) => {
is = false; is = false;
Storage.setAllowed(data.localStorage);
timeout(() => { callback(null, data);
timeout(() => {
if (!is)
Promises.config = null;
});
}, () => {
if (!is) if (!is)
Promises.config = null; Promises.config = null;
}); });
}
return data;
};
function getTimeoutOnce(time) { function getTimeoutOnce(time) {
let is; let is;
@ -134,3 +165,4 @@ function getTimeoutOnce(time) {
}, time); }, time);
}; };
} }

View file

@ -1,13 +1,21 @@
/* global CloudCmd */
/* global DOM */ /* global DOM */
import createElement from '@cloudcmd/create-element';
'use strict';
const Images = module.exports;
const LOADING = 'loading'; const LOADING = 'loading';
const HIDDEN = 'hidden'; const HIDDEN = 'hidden';
const ERROR = 'error'; const ERROR = 'error';
const getLoadingType = () => isSVG() ? '-svg' : '-gif'; const LoadingImage = LOADING + getLoadingType();
export const get = getElement; function getLoadingType() {
return isSVG() ? '-svg' : '-gif';
}
module.exports.get = getElement;
/** /**
* check SVG SMIL animation support * check SVG SMIL animation support
@ -26,54 +34,48 @@ function isSVG() {
return /SVGAnimate/.test(name); return /SVGAnimate/.test(name);
} }
function getElement() { function getElement() {
return createElement('span', { return DOM.load({
id: 'js-status-image', name : 'span',
className: 'icon', id : 'js-status-image',
dataName: 'progress', className : 'icon',
notAppend: true, attribute : 'data-progress',
notAppend : true
}); });
} }
/* Функция создаёт картинку загрузки */ /* Функция создаёт картинку загрузки */
export const loading = () => { module.exports.loading = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const classList = element.classList;
const loadingImage = LOADING + getLoadingType();
classList.add(LOADING, loadingImage); classList.add(LOADING, LoadingImage);
classList.remove(ERROR, HIDDEN); classList.remove(ERROR, HIDDEN);
return element; return element;
}; };
/* Функция создаёт картинку ошибки загрузки */ /* Функция создаёт картинку ошибки загрузки */
export const error = () => { module.exports.error = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const classList = element.classList;
const loadingImage = LOADING + getLoadingType();
classList.add(ERROR); classList.add(ERROR);
classList.remove(HIDDEN, LOADING, loadingImage); classList.remove(HIDDEN, LOADING, LoadingImage);
return element; return element;
}; };
show.load = show; module.exports.show = show;
show.error = (text) => { module.exports.show.load = show;
const image = Images.error(); module.exports.show.error = error;
DOM.show(image);
image.title = text;
return image;
};
/** /**
* Function shows loading spinner * Function shows loading spinner
* position = {top: true}; * position = {top: true};
*/ */
export function show(position, panel) { function show(position, panel) {
const image = Images.loading(); const image = Images.loading();
const parent = image.parentElement; const parent = image.parentElement;
const refreshButton = DOM.getRefreshButton(panel); const refreshButton = DOM.getRefreshButton(panel);
@ -91,7 +93,7 @@ export function show(position, panel) {
current = refreshButton.parentElement; current = refreshButton.parentElement;
} }
if (!parent || parent && parent !== current) if (!parent || (parent && parent !== current))
current.appendChild(image); current.appendChild(image);
DOM.show(image); DOM.show(image);
@ -99,25 +101,36 @@ export function show(position, panel) {
return image; return image;
} }
function error(text) {
const image = Images.error();
DOM.show(image);
image.title = text;
CloudCmd.log(text);
return image;
}
/** /**
* hide load image * hide load image
*/ */
export function hide() { module.exports.hide = () => {
const element = Images.get(); const element = Images.get();
DOM.hide(element); DOM.hide(element);
return Images; return Images;
} };
export const setProgress = (value, title) => { module.exports.setProgress = (value, title) => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
if (!element) if (!element)
return Images; return Images;
element.setAttribute(DATA, `${value}%`); element.setAttribute(DATA, value + '%');
if (title) if (title)
element.title = title; element.title = title;
@ -125,7 +138,7 @@ export const setProgress = (value, title) => {
return Images; return Images;
}; };
export const clearProgress = () => { module.exports.clearProgress = () => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
@ -138,12 +151,3 @@ export const clearProgress = () => {
return Images; return Images;
}; };
const Images = {
clearProgress,
setProgress,
show,
hide,
get,
error,
loading,
};

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
import {test, stub} from 'supertape';
import {getCSSVar, goToDirectory} from '#dom';
globalThis.CloudCmd = {};
test('cloudcmd: client: dom: goToDirectory', async (t) => {
const path = '';
const changeDir = stub();
const prompt = stub().returns([null, path]);
await goToDirectory({
prompt,
changeDir,
});
t.calledWith(changeDir, [path]);
t.end();
});
test('cloudcmd: client: dom: getCSSVar', (t) => {
const body = {};
const getPropertyValue = stub().returns(0);
globalThis.getComputedStyle = stub().returns({
getPropertyValue,
});
const result = getCSSVar('hello', {
body,
});
delete globalThis.getComputedStyle;
t.notOk(result);
t.end();
});
test('cloudcmd: client: dom: getCSSVar: 1', (t) => {
const body = {};
const getPropertyValue = stub().returns(1);
globalThis.getComputedStyle = stub().returns({
getPropertyValue,
});
const result = getCSSVar('hello', {
body,
});
delete globalThis.getComputedStyle;
t.ok(result);
t.end();
});

View file

@ -1,166 +0,0 @@
import {FS} from '#common/cloudfunc';
import {sendRequest as _sendRequest} from './send-request.js';
const {assign} = Object;
const imgPosition = {
top: true,
};
export const remove = async (url, data, overrides = {}) => {
const {
sendRequest = _sendRequest,
} = overrides;
const request = {
method: 'DELETE',
url: FS + url,
imgPosition: {
top: Boolean(data),
},
};
if (data)
assign(request, {
data,
url: `${request.url}?files`,
});
return await sendRequest(request);
};
export const patch = async (url, data) => {
return await _sendRequest({
method: 'PATCH',
url: FS + url,
data,
imgPosition,
});
};
export const write = async (url, data) => {
return await _sendRequest({
method: 'PUT',
url: FS + url,
data,
imgPosition,
});
};
export const createDirectory = async (url, overrides = {}) => {
const {
sendRequest = _sendRequest,
} = overrides;
return await sendRequest({
method: 'PUT',
url: `${FS}${url}?dir`,
imgPosition,
});
};
export const read = async (url, dataType = 'text') => {
const notLog = !url.includes('?');
return await _sendRequest({
method: 'GET',
url: FS + url,
notLog,
dataType,
});
};
export const copy = async (from, to, names) => {
return await _sendRequest({
method: 'PUT',
url: '/copy',
data: {
from,
to,
names,
},
imgPosition,
});
};
export const pack = async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/pack',
data,
});
};
export const extract = async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/extract',
data,
});
};
export const move = async (from, to, names) => {
return await _sendRequest({
method: 'PUT',
url: '/move',
data: {
from,
to,
names,
},
imgPosition,
});
};
export const rename = async (from, to) => {
return await _sendRequest({
method: 'PUT',
url: '/rename',
data: {
from,
to,
},
imgPosition,
});
};
export const Config = {
read: async () => {
return await _sendRequest({
method: 'GET',
url: '/config',
imgPosition,
notLog: true,
});
},
write: async (data) => {
return await _sendRequest({
method: 'PATCH',
url: '/config',
data,
imgPosition,
});
},
};
export const Markdown = {
read: async (url) => {
return await _sendRequest({
method: 'GET',
url: `/markdown${url}`,
imgPosition,
notLog: true,
});
},
render: async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/markdown',
data,
imgPosition,
notLog: true,
});
},
};

View file

@ -1,61 +0,0 @@
import {test, stub} from 'supertape';
import * as io from './index.js';
test('client: dom: io', (t) => {
const sendRequest = stub();
io.createDirectory('/hello', {
sendRequest,
});
const expected = {
imgPosition: {
top: true,
},
method: 'PUT',
url: '/fs/hello?dir',
};
t.calledWith(sendRequest, [expected]);
t.end();
});
test('client: dom: io: remove: no files', async (t) => {
const sendRequest = stub();
await io.remove('/hello', null, {
sendRequest,
});
const expected = {
imgPosition: {
top: false,
},
method: 'DELETE',
url: '/fs/hello',
};
t.calledWith(sendRequest, [expected]);
t.end();
});
test('client: dom: io: remove: files', async (t) => {
const sendRequest = stub();
const files = ['world'];
await io.remove('/hello', files, {
sendRequest,
});
const expected = {
imgPosition: {
top: true,
},
data: ['world'],
method: 'DELETE',
url: '/fs/hello?files',
};
t.calledWith(sendRequest, [expected]);
t.end();
});

View file

@ -1,48 +0,0 @@
/* global CloudCmd */
import {promisify} from 'es6-promisify';
import * as Images from '#dom/images';
import * as load from '#dom/load';
export const sendRequest = promisify((params, callback) => {
const p = params;
const {prefixURL} = CloudCmd;
p.url = prefixURL + p.url;
p.url = encodeURI(p.url);
p.url = replaceHash(p.url);
load.ajax({
method: p.method,
url: p.url,
data: p.data,
dataType: p.dataType,
error: (jqXHR) => {
const response = jqXHR.responseText;
const {statusText, status} = jqXHR;
const text = status === 404 ? response : statusText;
callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
callback(null, data);
},
});
});
export const _replaceHash = replaceHash;
function replaceHash(url) {
/*
* if we send ajax request -
* no need in hash so we escape #
*/
return url.replace(/#/g, '%23');
}

View file

@ -1,11 +0,0 @@
import {test} from 'supertape';
import {_replaceHash} from './send-request.js';
test('cloudcmd: client: io: replaceHash', (t) => {
const url = '/hello/####world';
const result = _replaceHash(url);
const expected = '/hello/%23%23%23%23world';
t.equal(result, expected);
t.end();
});

View file

@ -1,58 +1,69 @@
/* global CloudCmd */ 'use strict';
import {callbackify} from 'node:util';
import {rendy} from 'rendy';
import itype from 'itype';
import * as load from 'load.js';
import {tryToCatch} from 'try-to-catch';
import {findObjByNameInArr} from '#common/util';
import * as Files from '#dom/files';
export const loadRemote = callbackify(async (name, options) => { /* global CloudCmd */
const {prefix, config} = CloudCmd;
const exec = require('execon');
const rendy = require('rendy/legacy');
const itype = require('itype/legacy');
const {findObjByNameInArr} = require('../../common/util');
const load = require('./load');
const Files = require('./files');
module.exports = (name, options, callback = options) => {
const {PREFIX, config} = CloudCmd;
const o = options; const o = options;
if (o.name && window[o.name]) if (o.name && window[o.name])
return; return callback();
const modules = await Files.get('modules'); Files.get('modules', (error, modules) => {
const online = config('online') && navigator.onLine;
const module = findObjByNameInArr(modules.remote, name);
const online = config('online') && navigator.onLine; const isArray = itype.array(module.local);
const module = findObjByNameInArr(modules.remote, name); const version = module.version;
const isArray = itype.array(module.local); let remoteTmpls, local;
const {version} = module; if (isArray) {
remoteTmpls = module.remote;
local = module.local;
} else {
remoteTmpls = [module.remote];
local = [module.local];
}
let remoteTmpls; const localURL = local.map((url) => {
let local; return PREFIX + url;
});
if (isArray) { const remoteURL = remoteTmpls.map((tmpl) => {
remoteTmpls = module.remote; return rendy(tmpl, {
({local} = module); version: version
} else { });
remoteTmpls = [module.remote]; });
local = [module.local];
}
const localURL = []; const on = funcON(localURL, remoteURL, callback);
const off = funcOFF(localURL, callback);
for (const url of local) { exec.if(online, on, off);
localURL.push(prefix + url); });
} };
const remoteURL = []; function funcOFF(local, callback) {
return () => {
load.parallel(local, callback);
};
}
for (const tmpl of remoteTmpls) { function funcON (local, remote,callback) {
remoteURL.push(rendy(tmpl, { return () => {
version, load.parallel(remote, (error) => {
})); if (error)
} return funcOFF();
if (online) { callback();
const [e] = await tryToCatch(load.parallel, remoteURL); });
};
}
if (!e)
return;
}
await load.parallel(localURL);
});

View file

@ -1,8 +1,130 @@
import itype from 'itype'; 'use strict';
import jonny from 'jonny';
import Emitify from 'emitify'; const itype = require('itype/legacy');
import exec from 'execon'; const jonny = require('jonny/legacy');
import * as Images from '#dom/images'; const Emitify = require('emitify/legacy');
const exec = require('execon');
const Images = require('./images');
const Events = require('./events');
const {getExt} = require('../../common/util');
module.exports = load;
module.exports.getIdBySrc = getIdBySrc;
module.exports.ext = ext;
/**
* Функция создаёт элемент и загружает файл с src.
*
* @param params = {
* name, - название тэга
* src', - путь к файлу
* func, - обьект, содержаий одну из функций
* или сразу две onload и onerror
* {onload: function() {}, onerror: function();}
* style,
* id,
* element,
* async, - true by default
* inner: 'id{color:red, },
* class,
* notAppend - false by default
*
*/
function load(params) {
const {
src,
id = getIdBySrc(params.src),
func,
name,
async,
inner,
style,
parent = document.body,
className,
attribute,
notAppend,
} = params;
let element = document.getElementById(id);
if (element) {
exec(func);
return element;
}
element = document.createElement(name);
const funcError = () => {
const msg = `file ${src} could not be loaded`;
const error = new Error(msg);
parent.removeChild(element);
Images.show.error(msg);
const callback = func && func.onerror || func.onload || func;
exec(callback, error);
};
const funcLoad = () => {
const callback = func && func.onload || func;
Events.remove('error', element, funcError);
exec(callback);
};
if (/^(script|link)$/.test(name))
Events.addOnce('load', element, funcLoad)
.addError(element, funcError);
if (id)
element.id = id;
if (className)
element.className = className;
if (src) {
if (name !== 'link') {
element.src = src;
} else {
element.href = src;
element.rel = 'stylesheet';
}
}
if (attribute) {
const type = itype(attribute);
switch(type) {
case 'string':
element.setAttribute(attribute, '');
break;
case 'object':
Object.keys(attribute).forEach((name) => {
element.setAttribute(name, attribute[name]);
});
break;
}
}
if (style)
element.style.cssText = style;
if (async && name === 'script' || async === undefined)
element.async = true;
if (!notAppend)
parent.appendChild(element);
if (inner)
element.innerHTML = inner;
return element;
}
/** /**
* Function gets id by src * Function gets id by src
@ -10,18 +132,17 @@ import * as Images from '#dom/images';
* *
* Example: http://domain.com/1.js -> 1_js * Example: http://domain.com/1.js -> 1_js
*/ */
export function getIdBySrc(src) { function getIdBySrc(src) {
const isStr = itype.string(src); const isStr = itype.string(src);
if (!isStr) if (!isStr)
return; return;
if (src.includes(':')) if (~src.indexOf(':'))
src += '-join'; src += '-join';
const num = src.lastIndexOf('/') + 1; const num = src.lastIndexOf('/') + 1;
const sub = src.substr(src, num); const sub = src.substr(src, num);
const id = src const id = src
.replace(sub, '') .replace(sub, '')
.replace(/\./g, '-'); .replace(/\./g, '-');
@ -34,37 +155,34 @@ export function getIdBySrc(src) {
* *
* @param params * @param params
*/ */
export const ajax = (params) => { module.exports.ajax = (params) => {
const p = params; const p = params;
const isObject = itype.object(p.data); const isObject = itype.object(p.data);
const isArray = itype.array(p.data); const isArray = itype.array(p.data);
const isArrayBuf = itype(p.data) === 'arraybuffer'; const isArrayBuf = itype(p.data) === 'arraybuffer';
const type = p.type || p.method || 'GET'; const type = p.type || p.method || 'GET';
const headers = p.headers || {};
const {headers = {}} = p;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open(type, p.url, true); xhr.open(type, p.url, true);
for (const name of Object.keys(headers)) { Object.keys(headers).forEach((name) => {
const value = headers[name]; const value = headers[name];
xhr.setRequestHeader(name, value); xhr.setRequestHeader(name, value);
} });
if (p.responseType) if (p.responseType)
xhr.responseType = p.responseType; xhr.responseType = p.responseType;
let data; let data;
if (!isArrayBuf && isObject || isArray) if (!isArrayBuf && isObject || isArray)
data = jonny.stringify(p.data); data = jonny.stringify(p.data);
else else
({data} = p); data = p.data;
xhr.onreadystatechange = (event) => { xhr.onreadystatechange = (event) => {
const xhr = event.target; const xhr = event.target;
const OK = 200; const OK = 200;
if (xhr.readyState !== xhr.DONE) if (xhr.readyState !== xhr.DONE)
return; return;
@ -72,16 +190,15 @@ export const ajax = (params) => {
Images.clearProgress(); Images.clearProgress();
const TYPE_JSON = 'application/json'; const TYPE_JSON = 'application/json';
const type = xhr.getResponseHeader('content-type'); const type = xhr.getResponseHeader('content-type');
if (xhr.status !== OK) if (xhr.status !== OK)
return exec(p.error, xhr); return exec(p.error, xhr);
const notText = p.dataType !== 'text'; const notText = p.dataType !== 'text';
const isContain = type.includes(TYPE_JSON); const isContain = ~type.indexOf(TYPE_JSON);
let data = xhr.response; let data = xhr.response;
if (type && isContain && notText) if (type && isContain && notText)
data = jonny.parse(xhr.response) || xhr.response; data = jonny.parse(xhr.response) || xhr.response;
@ -91,11 +208,12 @@ export const ajax = (params) => {
xhr.send(data); xhr.send(data);
}; };
export const put = (url, body) => { module.exports.put = (url, body) => {
const emitter = Emitify(); const emitter = Emitify();
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
url = encodeURI(url).replace(/#/g, '#'); url = encodeURI(url)
.replace('#', '%23');
xhr.open('put', url, true); xhr.open('put', url, true);
@ -103,8 +221,8 @@ export const put = (url, body) => {
if (!event.lengthComputable) if (!event.lengthComputable)
return; return;
const percent = event.loaded / event.total * 100; const percent = (event.loaded / event.total) * 100;
const count = Math.round(percent); const count = Math.round(percent);
emitter.emit('progress', count); emitter.emit('progress', count);
}; };
@ -116,10 +234,8 @@ export const put = (url, body) => {
if (!over) if (!over)
return; return;
if (xhr.status === OK) { if (xhr.status === OK)
emitter.emit('progress', 100);
return emitter.emit('end'); return emitter.emit('end');
}
const error = Error(xhr.responseText); const error = Error(xhr.responseText);
emitter.emit('error', error); emitter.emit('error', error);
@ -129,3 +245,117 @@ export const put = (url, body) => {
return emitter; return emitter;
}; };
function ext(src, func) {
switch (getExt(src)) {
case '.js':
return load.js(src, func);
case '.css':
return load.css(src, func);
default:
return load({
src,
func,
});
}
}
/**
* create elements and load them to DOM-tree
* one-by-one
*
* @param params
* @param callback
*/
load.series = (params, callback) => {
if (!params)
return load;
const funcs = params
.map((url) => ext.bind(null, url))
.concat(callback);
exec.series(funcs);
return load;
};
/**
* improve callback of funcs so
* we pop number of function and
* if it's last we call pCallBack
*
* @param params
* @param callback - onload function
*/
load.parallel = (params, callback) => {
if (!params)
return load;
const funcs = params.map((url) => {
return ext.bind(null, url);
});
exec.parallel(funcs, callback);
return load;
};
/**
* Функция загружает js-файл
*
* @param src
* @param func
*/
load.js = (src, func) => {
const name = 'script';
return load({
name,
src,
func,
});
},
load.css = (src, func) => {
const name = 'link';
const {head:parent} = document;
return load({
name,
src,
parent,
func
});
};
/**
* Функция создаёт елемент style и записывает туда стили
* @param params - структура параметров, заполняеться таким
* образом: {src: ' ',func: '', id: '', element: '', inner: ''}
* все параметры опциональны
*/
load.style = (params) => {
const {
id,
src,
name = 'style',
func,
inner,
parent = document.head,
element,
} = params;
return load({
id,
src,
func,
name,
inner,
parent,
element,
});
};

View file

@ -1,66 +0,0 @@
/* global CloudCmd */
import capitalize from 'just-capitalize';
import * as _Dialog from '#dom/dialog';
import * as Storage from '#dom/storage';
import * as RESTful from '#dom/rest';
import * as _currentFile from '../current-file.js';
export default async (current, overrides = {}) => {
const {
refresh = CloudCmd.refresh,
Dialog = _Dialog,
currentFile = _currentFile,
} = overrides;
const {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
} = currentFile;
if (!isCurrentFile(current))
current = getCurrentFile();
const from = getCurrentName(current);
if (from === '..')
return Dialog.alert.noFiles();
const [cancel, to] = await Dialog.prompt('Rename', from);
if (cancel)
return;
const nextFile = getCurrentByName(to);
if (nextFile) {
const type = getCurrentType(nextFile);
const msg = `${capitalize(type)} "${to}" already exists. Proceed?`;
const [cancel] = await Dialog.confirm(msg);
if (cancel)
return;
}
if (from === to)
return;
const dirPath = getCurrentDirPath();
const fromFull = `${dirPath}${from}`;
const toFull = `${dirPath}${to}`;
const [e] = await RESTful.rename(fromFull, toFull);
if (e)
return;
setCurrentName(to, current);
Storage.remove(dirPath);
refresh();
};

View file

@ -1,86 +0,0 @@
import {test, stub} from 'supertape';
import renameCurrent from './rename-current.js';
test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => {
const current = {};
const isCurrentFile = stub();
const currentFile = stubCurrentFile({
isCurrentFile,
});
await renameCurrent(current, {
Dialog: stubDialog(),
currentFile,
});
t.calledWith(isCurrentFile, [current], 'should call isCurrentFile');
t.end();
});
test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const current = {};
const name = 'hello';
const prompt = stub().returns([null, name]);
const confirm = stub().returns([true]);
const getCurrentByName = stub().returns(current);
const getCurrentType = stub().returns('directory');
const Dialog = stubDialog({
confirm,
prompt,
});
const currentFile = stubCurrentFile({
getCurrentByName,
getCurrentType,
});
await renameCurrent(null, {
Dialog,
currentFile,
});
const expected = 'Directory "hello" already exists. Proceed?';
t.calledWith(confirm, [expected], 'should call confirm');
t.end();
});
const stubDialog = (fns = {}) => {
const {
alert = stub().returns([]),
confirm = stub().returns([]),
prompt = stub().returns([]),
} = fns;
return {
alert,
confirm,
prompt,
};
};
const stubCurrentFile = (fns = {}) => {
const {
isCurrentFile = stub(),
getCurrentName = stub(),
getCurrentFile = stub(),
getCurrentByName = stub(),
getCurrentType = stub(),
getCurrentDirPath = stub(),
setCurrentName = stub(),
} = fns;
return {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
};
};

View file

@ -1,40 +1,220 @@
import {tryToCatch} from 'try-to-catch'; 'use strict';
import * as Dialog from '#dom/dialog';
import * as Images from '#dom/images';
import {encode} from '#common/entity';
import * as IO from './io/index.js';
const handleError = (promise) => async (...args) => { /* global CloudCmd, DOM */
const [e, data] = await tryToCatch(promise, ...args);
if (!e) const itype = require('itype/legacy');
return [e, data];
const encoded = encode(e.message); const {FS} = require('../../common/cloudfunc');
const {
encode,
} = require('../../common/entity');
Images.show.error(encoded); module.exports = new RESTful();
Dialog.alert(encoded);
return [e, data]; const Images = require('./images');
}; const load = require('./load');
export const remove = handleError(IO.remove); function RESTful() {
export const patch = handleError(IO.patch); this.delete = (url, data, callback) => {
export const write = handleError(IO.write); const isFunc = itype.function(data);
export const createDirectory = handleError(IO.createDirectory);
export const read = handleError(IO.read);
export const copy = handleError(IO.copy);
export const pack = handleError(IO.pack);
export const extract = handleError(IO.extract);
export const move = handleError(IO.move);
export const rename = handleError(IO.rename);
export const Config = { if (!callback && isFunc) {
read: handleError(IO.Config.read), callback = data;
write: handleError(IO.Config.write), data = null;
}; }
export const Markdown = { sendRequest({
read: handleError(IO.Markdown.read), method : 'DELETE',
render: handleError(IO.Markdown.render), url : FS + url,
}; data,
callback,
imgPosition : { top: !!data }
});
};
this.patch = (url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
const imgPosition = {
top: true
};
sendRequest({
method: 'PATCH',
url: FS + url,
data,
callback,
imgPosition,
});
};
this.write = (url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PUT',
url: FS + url,
data,
callback,
imgPosition : { top: true }
});
};
this.read = (url, dataType, callback) => {
const isQuery = /\?/.test(url);
const isBeautify = /\?beautify$/.test(url);
const isMinify = /\?minify$/.test(url);
const notLog = !isQuery || isBeautify || isMinify;
const isFunc = itype.function(dataType);
if (!callback && isFunc) {
callback = dataType;
dataType = 'text';
}
sendRequest({
method: 'GET',
url: FS + url,
callback,
notLog,
dataType,
});
};
this.cp = (data, callback) => {
sendRequest({
method: 'PUT',
url: '/cp',
data,
callback,
imgPosition : { top: true }
});
};
this.pack = (data, callback) => {
sendRequest({
method : 'PUT',
url : '/pack',
data,
callback,
});
};
this.extract = function(data, callback) {
sendRequest({
method : 'PUT',
url : '/extract',
data : data,
callback : callback
});
};
this.mv = function(data, callback) {
sendRequest({
method : 'PUT',
url : '/mv',
data : data,
callback : callback,
imgPosition : { top: true }
});
};
this.Config = {
read: function(callback) {
sendRequest({
method : 'GET',
url : '/config',
callback : callback,
imgPosition : { top: true },
notLog : true
});
},
write: function(data, callback) {
sendRequest({
method : 'PATCH',
url : '/config',
data : data,
callback : callback,
imgPosition : { top: true }
});
}
};
this.Markdown = {
read : function(url, callback) {
sendRequest({
method : 'GET',
url : '/markdown' + url,
callback : callback,
imgPosition : { top: true },
notLog : true
});
},
render : function(data, callback) {
sendRequest({
method : 'PUT',
url : '/markdown',
data : data,
callback : callback,
imgPosition : { top: true },
notLog : true
});
}
};
function sendRequest(params) {
const p = params;
const prefixUrl = CloudCmd.PREFIX_URL;
p.url = prefixUrl + p.url;
p.url = encodeURI(p.url);
/*
* if we send ajax request -
* no need in hash so we escape #
*/
p.url = p.url.replace('#', '%23');
load.ajax({
method : p.method,
url : p.url,
data : p.data,
dataType : p.dataType,
error : (jqXHR) => {
const response = jqXHR.responseText;
const statusText = jqXHR.statusText;
const status = jqXHR.status;
const text = status === 404 ? response : statusText;
const encoded = encode(text);
Images.show.error(encoded);
setTimeout(() => {
DOM.Dialog.alert(CloudCmd.TITLE, encoded);
}, 100);
p.callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
p.callback(null, data);
}
});
}
}

View file

@ -1,46 +1,52 @@
import {alert, prompt} from '#dom/dialog'; 'use strict';
import {getRegExp} from '#common/util';
import {getCurrentName} from './current-file.js'; /* global DOM */
import {
isSelected,
toggleSelectedFile,
} from './cmd.js';
let SelectType = '*.*'; let SelectType = '*.*';
const TITLE = 'Cloud Commander';
export const selectByPattern = async (msg, files) => { const {getRegExp} = require('../../common/util');
if (!files)
return;
module.exports = (msg, files) => {
const allMsg = `Specify file type for ${msg} selection`; const allMsg = `Specify file type for ${msg} selection`;
const [cancel, type] = await prompt(allMsg, SelectType); const cancel = false;
const {Dialog} = DOM;
if (cancel) Dialog.prompt(TITLE, allMsg, SelectType, {cancel}).then((type) => {
return; SelectType = type;
SelectType = type; const regExp = getRegExp(type);
const regExp = getRegExp(type); if (!files)
let matches = 0; return;
for (const current of files) { let matches = 0;
const name = getCurrentName(current);
if (name === '..' || !regExp.test(name)) files.forEach((current) => {
continue; const name = DOM.getCurrentName(current);
++matches; if (name === '..')
return;
let selected = isSelected(current); const isMatch = regExp.test(name);
const shouldSel = msg === 'expand';
if (shouldSel) if (!isMatch)
selected = !selected; return;
if (selected) ++matches;
toggleSelectedFile(current);
}
if (!matches) let isSelected = DOM.isSelected(current);
alert('No matches found!'); const shouldSel = msg === 'expand';
if (shouldSel)
isSelected = !isSelected;
if (isSelected)
DOM.toggleSelectedFile(current);
});
if (!matches)
Dialog.alert('Select Files', 'No matches found!');
});
}; };

View file

@ -1,26 +1,84 @@
const {parse, stringify} = JSON; 'use strict';
export const set = (name, data) => { const itype = require('itype/legacy');
localStorage.setItem(name, data); const jonny = require('jonny/legacy');
const exec = require('execon');
const tryCatch = require('try-catch');
const setItem = localStorage.setItem.bind(localStorage);
/* приватный переключатель возможности работы с кэшем */
let Allowed;
/**
* allow Storage usage
*/
module.exports.setAllowed = (isAllowed) => {
Allowed = isAllowed;
}; };
export const setJson = (name, data) => { /** remove element */
localStorage.setItem(name, stringify(data)); module.exports.remove = (item, callback) => {
if (Allowed)
localStorage.removeItem(item);
exec(callback, null, Allowed);
return module.exports;
}; };
export const get = (name) => { module.exports.removeMatch = (string, callback) => {
return localStorage.getItem(name); const reg = RegExp('^' + string + '.*$');
const test = (a) => reg.test(a);
const remove = (a) => localStorage.removeItem(a);
Object.keys(localStorage)
.filter(test)
.forEach(remove);
exec(callback);
return module.exports;
}; };
export const getJson = (name) => { /** если доступен localStorage и
const data = localStorage.getItem(name); * в нём есть нужная нам директория -
return parse(data); * записываем данные в него
*/
module.exports.set = (name, data, callback) => {
let str, error;
if (itype.object(data))
str = jonny.stringify(data);
if (Allowed && name)
[error] = tryCatch(setItem, name, str || data);
exec(callback, error);
return module.exports;
},
/** Если доступен Storage принимаем из него данные*/
module.exports.get = (name, callback) => {
let ret;
if (Allowed)
ret = localStorage.getItem(name);
exec(callback, null, ret);
return module.exports;
},
/** функция чистит весь кэш для всех каталогов*/
module.exports.clear = (callback) => {
const ret = Allowed;
if (ret)
localStorage.clear();
exec(callback, null, ret);
return module.exports;
}; };
export const clear = () => {
localStorage.clear();
};
export const remove = (item) => {
localStorage.removeItem(item);
};

View file

@ -1,105 +0,0 @@
import {test, stub} from 'supertape';
import * as storage from '#dom/storage';
const {stringify} = JSON;
test('cloudcmd: client: storage: set', async (t) => {
const {localStorage} = globalThis;
const setItem = stub();
globalThis.localStorage = {
setItem,
};
await storage.set('hello', 'world');
globalThis.localStorage = localStorage;
t.calledWith(setItem, ['hello', 'world'], 'should call setItem');
t.end();
});
test('cloudcmd: client: storage: get', async (t) => {
const {localStorage} = globalThis;
const getItem = stub().returns('world');
globalThis.localStorage = {
getItem,
};
const result = await storage.get('hello');
globalThis.localStorage = localStorage;
t.equal(result, 'world');
t.end();
});
test('cloudcmd: client: storage: getJson', async (t) => {
const {localStorage} = globalThis;
const expected = {
hello: 'world',
};
const getItem = stub().returns(stringify(expected));
globalThis.localStorage = {
getItem,
};
const result = await storage.getJson('hello');
globalThis.localStorage = localStorage;
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: storage: setJson', async (t) => {
const {localStorage} = globalThis;
const data = {
hello: 'world',
};
const expected = stringify(data);
const setItem = stub();
globalThis.localStorage = {
setItem,
};
await storage.setJson('hello', data);
globalThis.localStorage = localStorage;
t.calledWith(setItem, ['hello', expected]);
t.end();
});
test('cloudcmd: client: storage: remove', async (t) => {
const {localStorage} = globalThis;
const removeItem = stub();
globalThis.localStorage = {
removeItem,
};
await storage.remove('hello');
globalThis.localStorage = localStorage;
t.calledWith(removeItem, ['hello'], 'should call removeItem');
t.end();
});
test('cloudcmd: client: storage: clear', async (t) => {
const {localStorage} = globalThis;
const clear = stub();
globalThis.localStorage = {
clear,
};
await storage.clear();
globalThis.localStorage = localStorage;
t.calledWithNoArgs(clear, 'should call clear');
t.end();
});

View file

@ -1,19 +1,27 @@
'use strict';
/* global CloudCmd */ /* global CloudCmd */
import {eachSeries} from 'execon';
import wraptile from 'wraptile';
import * as load from '#dom/load';
import {alert} from '#dom/dialog';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images';
import {getCurrentDirPath} from './current-file.js';
const loadFile = wraptile(_loadFile); const {eachSeries} = require('execon');
const wraptile = require('wraptile/legacy');
const DOM = require('.');
const load = require('./load');
const Images = require('./images');
const {FS} = require('../../common/cloudfunc');
const onEnd = wraptile(_onEnd); const onEnd = wraptile(_onEnd);
const loadFile = wraptile(_loadFile);
export const uploadFiles = (dir, files) => { const {
getCurrentDirPath: getPathWhenRootEmpty
} = DOM;
module.exports = (dir, files) => {
if (!files) { if (!files) {
files = dir; files = dir;
dir = getCurrentDirPath(); dir = getPathWhenRootEmpty();
} }
const n = files.length; const n = files.length;
@ -21,7 +29,7 @@ export const uploadFiles = (dir, files) => {
if (!n) if (!n)
return; return;
const array = Array.from(files); const array = [...files];
const {name} = files[0]; const {name} = files[0];
eachSeries(array, loadFile(dir, n), onEnd(name)); eachSeries(array, loadFile(dir, n), onEnd(name));
@ -29,17 +37,17 @@ export const uploadFiles = (dir, files) => {
function _onEnd(currentName) { function _onEnd(currentName) {
CloudCmd.refresh({ CloudCmd.refresh({
currentName, currentName
}); });
} }
function _loadFile(dir, n, file, callback) { function _loadFile(dir, n, file, callback) {
let i = 0; let i = 0;
const {name} = file; const name = file.name;
const path = dir + name; const path = dir + name;
const {prefixURL} = CloudCmd; const {PREFIX_URL} = CloudCmd;
const api = prefixURL + FS; const api = PREFIX_URL + FS;
const percent = (i, n, per = 100) => { const percent = (i, n, per = 100) => {
return Math.round(i * per / n); return Math.round(i * per / n);
@ -49,9 +57,7 @@ function _loadFile(dir, n, file, callback) {
++i; ++i;
load load.put(api + path, file)
.put(api + path, file)
.on('error', showError)
.on('end', callback) .on('end', callback)
.on('progress', (count) => { .on('progress', (count) => {
const max = step(n); const max = step(n);
@ -62,6 +68,3 @@ function _loadFile(dir, n, file, callback) {
}); });
} }
function showError({message}) {
alert(message);
}

View file

@ -1,44 +0,0 @@
/* global DOM */
/**
* Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень
*/
export const getJsonFromFileTable = () => {
const Info = DOM.CurrentInfo;
const path = DOM.getCurrentDirPath();
const infoFiles = Info.files || [];
const files = infoFiles
.filter(notParent)
.map(parse);
const fileTable = {
path,
files,
};
return fileTable;
};
const notParent = (current) => {
const name = DOM.getCurrentName(current);
return name !== '..';
};
const parse = (current) => {
const name = DOM.getCurrentName(current);
const size = DOM.getCurrentSize(current);
const owner = DOM.getCurrentOwner(current);
const mode = DOM.getCurrentMode(current);
const date = DOM.getCurrentDate(current);
const type = DOM.getCurrentType(current);
return {
name,
size,
mode,
owner,
date,
type,
};
};

80
client/input.js Normal file
View file

@ -0,0 +1,80 @@
'use strict';
const currify = require('currify/legacy');
const isType = currify((type, object, name) => {
return typeof object[name] === type;
});
const isBool = isType('boolean');
module.exports.getElementByName = getElementByName;
function getElementByName(selector, element) {
const str = `[data-name="js-${selector}"]`;
return element
.querySelector(str);
}
module.exports.getName = (element) => {
const name = element
.getAttribute('data-name')
.replace(/^js-/, '');
return name;
};
module.exports.convert = (config) => {
const result = Object.assign({}, config);
const array = Object.keys(result);
array
.filter(isBool(result))
.forEach((name) => {
const item = result[name];
result[name] = setState(item);
});
return result;
};
function setState(state) {
if (state)
return ' checked';
return '';
}
module.exports.getValue = (name, element) => {
const el = getElementByName(name, element);
const type = el.type;
switch(type) {
case 'checkbox':
return el.checked;
case 'number':
return Number(el.value);
default:
return el.value;
}
};
module.exports.setValue = (name, value, element) => {
const el = getElementByName(name, element);
const type = el.type;
switch(type) {
case 'checkbox':
el.checked = value;
break;
default:
el.value = value;
break;
}
};

View file

@ -1,15 +0,0 @@
export const createBinder = () => {
let binded = false;
return {
isBind() {
return binded;
},
setBind() {
binded = true;
},
unsetBind() {
binded = false;
},
};
};

View file

@ -1,28 +0,0 @@
import {test} from 'supertape';
import {createBinder} from './binder.js';
test('client: key: binder: isBind: default', (t) => {
const binder = createBinder();
t.notOk(binder.isBind(), 'should not be bind by default');
t.end();
});
test('client: key: binder: setBind', (t) => {
const binder = createBinder();
binder.setBind();
t.ok(binder.isBind(), 'should be bind');
t.end();
});
test('client: key: binder: unsetBind', (t) => {
const binder = createBinder();
binder.setBind();
binder.unsetBind();
t.notOk(binder.isBind(), 'should not be bind');
t.end();
});

View file

@ -1,544 +1,479 @@
/* global CloudCmd, DOM */ /* global CloudCmd, DOM */
import clipboard from '@cloudcmd/clipboard';
import {fullstore} from 'fullstore';
import * as Events from '#dom/events';
import * as Buffer from '../dom/buffer.js';
import * as KEY from './key.js';
import _vim from './vim/index.js';
import setCurrentByChar from './set-current-by-char.js';
import {createBinder} from './binder.js';
'use strict';
const Info = DOM.CurrentInfo;
const exec = require('execon');
const clipboard = require('@cloudcmd/clipboard');
const Events = require('../dom/events');
const Buffer = require('../dom/buffer');
const KEY = require('./key');
const vim = require('./vim');
const setCurrentByChar = require('./set-current-by-char');
const fullstore = require('fullstore/legacy');
const Chars = fullstore(); const Chars = fullstore();
const toggleVim = (keyCode, overrides = {}) => {
const {_config, config} = overrides;
if (!config('vim') && keyCode === KEY.ESC)
_config('vim', true);
};
const isUndefined = (a) => typeof a === 'undefined';
Chars([]); Chars([]);
const {assign} = Object; KeyProto.prototype = KEY;
const binder = createBinder(); CloudCmd.Key = KeyProto;
const bind = () => { function KeyProto() {
Events.addKey(listener, true); let Binded;
binder.setBind();
};
export const Key = assign(binder, KEY, { const Key = this;
bind,
});
export const _listener = listener; this.isBind = () => {
return Binded;
};
function getChar(event) { this.setBind = () => {
/* Binded = true;
* event.keyIdentifier deprecated in chrome v51 };
* but event.key is absent in chrome <= v51
*/
const {
key,
shift,
keyCode,
keyIdentifier,
} = event;
const char = key || fromCharCode(keyIdentifier); this.unsetBind = () => {
const symbol = getSymbol(shift, keyCode); Binded = false;
};
return [symbol, char]; this.bind = () => {
} Events.addKey(listener);
Binded = true;
};
async function listener(event, overrides = {}) { function getChar(event) {
const { /*
config = CloudCmd.config, * event.keyIdentifier deprecated in chrome v51
_config = CloudCmd._config, * but event.key is absent in chrome <= v51
switchKey = _switchKey, */
vim = _vim,
} = overrides;
const {keyCode} = event; if (event.key)
return event.key;
// strange chrome bug calls listener twice return fromCharCode(event.keyIdentifier);
// in second time event misses a lot fields
if (isUndefined(event.altKey))
return;
const alt = event.altKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
const [symbol, char] = getChar(event);
if (!binder.isBind())
return;
toggleVim(keyCode, {
config,
_config,
});
const isVim = config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol))
return setCurrentByChar(char, Chars);
Chars([]);
await switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
}
function getSymbol(shift, keyCode) {
switch(keyCode) {
case KEY.DOT:
return '.';
case KEY.HYPHEN:
return shift ? '_' : '-';
case KEY.EQUAL:
return shift ? '+' : '=';
} }
return ''; function listener(event) {
} const keyCode = event.keyCode;
const alt = event.altKey;
const ctrl = event.ctrlKey;
const shift = event.shiftKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
function fromCharCode(keyIdentifier) { let char = getChar(event);
const code = keyIdentifier.substring(2); let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char);
const hex = parseInt(code, 16);
return String.fromCharCode(hex); if (!isSymbol) {
} isSymbol = getSymbol(shift, keyCode);
async function _switchKey(event) { if (isSymbol)
const Info = DOM.CurrentInfo; char = isSymbol;
let i; }
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
const { if (!Key.isBind())
name, return;
panel,
path,
isDir,
} = Info;
const { const isVim = CloudCmd.config('vim');
Operation,
changeDir,
config,
} = CloudCmd;
const {keyCode} = event; if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
return setCurrentByChar(char, Chars);
const alt = event.altKey; Chars([]);
const shift = event.shiftKey; switchKey(event);
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
if (current) { if (isVim)
prev = current.previousSibling; vim(char, event);
next = current.nextSibling;
} }
switch(keyCode) { function getSymbol(shift, keyCode) {
case KEY.TAB: switch (keyCode) {
DOM.changePanel(); case KEY.DOT:
event.preventDefault(); return '.';
break;
case KEY.INSERT: case KEY.HYPHEN:
DOM return shift ? '_' : '-';
.toggleSelectedFile(current)
.setCurrentFile(next);
break;
case KEY.INSERT_MAC: case KEY.EQUAL:
DOM return shift ? '+' : '=';
.toggleSelectedFile(current) }
.setCurrentFile(next); }
break;
case KEY.DELETE: function fromCharCode(keyIdentifier) {
if (shift) const code = keyIdentifier.substring(2);
Operation.show('delete:silent'); const hex = parseInt(code, 16);
else const char = String.fromCharCode(hex);
Operation.show('delete');
break; return char;
}
case KEY.ASTERISK: function switchKey(event) {
DOM.toggleAllSelectedFiles(current); let i, isSelected, prev, next;
break; let current = Info.element;
let dataName;
case KEY.PLUS: const name = Info.name;
DOM.expandSelection();
event.preventDefault();
break;
case KEY.MINUS: const {Operation} = CloudCmd;
DOM.shrinkSelection(); const panel = Info.panel;
event.preventDefault(); const path = Info.path;
break; const isDir = Info.isDir;
case KEY.F1: const keyCode = event.keyCode;
CloudCmd.Help.show(); const alt = event.altKey;
event.preventDefault(); const shift = event.shiftKey;
break; const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
case KEY.F2: if (current) {
CloudCmd.UserMenu.show(); prev = current.previousSibling;
break; next = current.nextSibling;
}
case KEY.F3: switch (keyCode) {
event.preventDefault(); case Key.TAB:
DOM.changePanel();
if (Info.isDir)
await changeDir(path);
else if (shift)
CloudCmd.View.show(null, {
raw: true,
});
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
break;
case KEY.F4:
if (config('vim'))
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
event.preventDefault();
break;
case KEY.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
event.preventDefault();
break;
case KEY.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else
Operation.show('move');
event.preventDefault();
break;
case KEY.F7:
if (shift)
DOM.promptNewFile();
else
DOM.promptNewDir();
event.preventDefault();
break;
case KEY.F8:
Operation.show('delete');
event.preventDefault();
break;
case KEY.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
event.preventDefault();
break;
case KEY.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case KEY.TRA:
event.preventDefault();
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case KEY.SPACE:
event.preventDefault();
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
if (!isSelected)
await DOM.loadCurrentSize(current);
DOM.toggleSelectedFile(current);
break;
case KEY.U:
if (ctrlMeta) {
DOM.swapPanels();
event.preventDefault(); event.preventDefault();
} break;
break; case Key.INSERT:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
/* navigation on file table: * case Key.INSERT_MAC:
* in case of pressing button 'up', * DOM .toggleSelectedFile(current)
* select previous row */ .setCurrentFile(next);
case KEY.UP: break;
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev); case Key.DELETE:
event.preventDefault(); if (shift)
break; Operation.show('delete:silent');
/* in case of pressing button 'down', *
* select next row */
case KEY.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(next);
event.preventDefault();
break;
case KEY.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case KEY.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case KEY.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case KEY.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case KEY.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case KEY.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
case KEY.ENTER:
if (Info.isDir)
await changeDir(path);
else
CloudCmd.View.show();
break;
case KEY.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case KEY.BACKSLASH:
if (ctrlMeta)
await changeDir('/');
break;
case KEY.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case KEY.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case KEY.L:
if (ctrlMeta && shift) {
CloudCmd.logOut();
event.preventDefault();
}
break;
case KEY.M:
if (ctrlMeta) {
if (config('vim'))
CloudCmd.EditNamesVim.show();
else else
CloudCmd.EditNames.show(); Operation.show('delete');
break;
case Key.ASTERISK:
DOM.toggleAllSelectedFiles(current);
break;
case Key.PLUS:
DOM.expandSelection();
event.preventDefault();
break;
case Key.MINUS:
DOM.shrinkSelection();
event.preventDefault();
break;
case Key.F1:
CloudCmd.Help.show();
event.preventDefault();
break;
case Key.F2:
DOM.renameCurrent(current);
break;
case Key.F3:
if (shift)
CloudCmd.Markdown.show(path);
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
event.preventDefault(); event.preventDefault();
} break;
break; case Key.F4:
if (shift)
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
case KEY.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case KEY.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
event.preventDefault(); event.preventDefault();
} break;
break; case Key.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
case KEY.C:
if (ctrlMeta)
Buffer.copy();
break;
case KEY.X:
if (ctrlMeta)
Buffer.cut();
break;
case KEY.V:
if (ctrlMeta)
Buffer.paste();
break;
case KEY.Z:
if (ctrlMeta)
Buffer.clear();
break;
case KEY.COLON:
CloudCmd.CommandLine.show();
event.preventDefault();
break;
/* чистим хранилище */
case KEY.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
await DOM.Storage.clear();
CloudCmd.log('storage cleared');
event.preventDefault(); event.preventDefault();
} break;
break; case Key.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else
Operation.show('move');
case KEY.DOT: event.preventDefault();
if (meta && shift) { break;
const showDotFiles = !CloudCmd.config('showDotFiles');
CloudCmd._config('showDotFiles', showDotFiles); case Key.F7:
CloudCmd.refresh(); if (shift)
await DOM.RESTful.Config.write({ DOM.promptNewFile();
showDotFiles, else
DOM.promptNewDir();
event.preventDefault();
break;
case Key.F8:
Operation.show('delete');
event.preventDefault();
break;
case Key.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
event.preventDefault();
break;
case Key.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case Key.TRA:
event.preventDefault();
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case Key.SPACE:
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
exec.if(isSelected, () => {
DOM.toggleSelectedFile(current);
}, (callback) => {
DOM.loadCurrentSize(callback, current);
}); });
}
break; event.preventDefault();
break;
case Key.U:
if (ctrlMeta) {
DOM.swapPanels();
event.preventDefault();
}
break;
/* navigation on file table: *
* in case of pressing button 'up', *
* select previous row */
case Key.UP:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev);
event.preventDefault();
break;
/* in case of pressing button 'down', *
* select next row */
case Key.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(next);
event.preventDefault();
break;
case Key.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case Key.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case Key.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case Key.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case Key.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case Key.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* open directory */
case Key.ENTER:
if (Info.isDir)
CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/'
});
break;
case Key.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case Key.BACKSLASH:
if (ctrlMeta)
CloudCmd.loadDir({
path: '/'
});
break;
case Key.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case Key.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case Key.M:
if (ctrlMeta) {
CloudCmd.EditNames.show();
event.preventDefault();
}
break;
case Key.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case Key.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
event.preventDefault();
}
break;
case Key.C:
if (ctrlMeta)
Buffer.copy();
break;
case Key.X:
if (ctrlMeta)
Buffer.cut();
break;
case Key.V:
if (ctrlMeta)
Buffer.paste();
break;
case Key.Z:
if (ctrlMeta)
Buffer.clear();
break;
/* чистим хранилище */
case Key.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
DOM.Storage.clear(() => {
CloudCmd.log('storage cleared');
});
event.preventDefault();
}
break;
}
} }
} }

View file

@ -1,56 +0,0 @@
import autoGlobals from 'auto-globals';
import supertape from 'supertape';
import {ESC} from './key.js';
import {Key, _listener} from './index.js';
import {getDOM, getCloudCmd} from './vim/globals.fixture.js';
const test = autoGlobals(supertape);
const {stub} = supertape;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
test('cloudcmd: client: key: enable vim', async (t) => {
const vim = stub();
const config = stub().returns(true);
const _config = stub();
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
Key.setBind();
await _listener(event, {
vim,
config,
_config,
switchKey: stub(),
});
t.calledWith(vim, ['Escape', event]);
t.end();
});
test('cloudcmd: client: key: disable vim', async (t) => {
const _config = stub();
const config = stub();
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
Key.setBind();
await _listener(event, {
config,
_config,
switchKey: stub(),
});
t.calledWith(_config, ['vim', true]);
t.end();
});

View file

@ -1,58 +1,81 @@
export const BACKSPACE = 8; 'use strict';
export const TAB = 9;
export const ENTER = 13; module.exports = {
export const CAPSLOCK = 20; BACKSPACE : 8,
export const ESC = 27; TAB : 9,
export const SPACE = 32; ENTER : 13,
export const PAGE_UP = 33; ESC : 27,
export const PAGE_DOWN = 34;
export const END = 35; SPACE : 32,
export const HOME = 36; PAGE_UP : 33,
export const LEFT = 37; PAGE_DOWN : 34,
export const UP = 38; END : 35,
export const RIGHT = 39; HOME : 36,
export const DOWN = 40;
export const INSERT = 45; LEFT : 37,
export const DELETE = 46; UP : 38,
export const ZERO = 48; RIGHT : 39,
export const SEMICOLON = 52; DOWN : 40,
export const A = 65;
export const C = 67; INSERT : 45,
export const D = 68; DELETE : 46,
export const G = 71;
export const J = 74; ZERO : 48,
export const K = 75;
export const L = 76; SEMICOLON : 52,
export const M = 77;
export const O = 79; COLON : 54,
export const P = 80;
export const Q = 81; A : 65,
export const R = 82;
export const S = 83; C : 67,
export const T = 84; D : 68,
export const U = 85;
export const V = 86; G : 71,
export const X = 88;
export const Z = 90; J : 74,
export const INSERT_MAC = 96; K : 75,
export const ASTERISK = 106;
export const PLUS = 107; M : 77,
export const MINUS = 109;
export const F1 = 112; O : 79,
export const F2 = 113; P : 80,
export const F3 = 114; Q : 81,
export const F4 = 115; R : 82,
export const F5 = 116; S : 83,
export const F6 = 117; T : 84,
export const F7 = 118; U : 85,
export const F8 = 119;
export const F9 = 120; V : 86,
export const F10 = 121;
export const COLON = 186; X : 88,
export const EQUAL = 187;
export const HYPHEN = 189; Z : 90,
export const DOT = 190;
export const SLASH = 191; INSERT_MAC : 96,
export const TRA = 192;
export const BACKSLASH = 220; ASTERISK : 106,
export const BRACKET_CLOSE = 221; PLUS : 107,
MINUS : 109,
F1 : 112,
F2 : 113,
F3 : 114,
F4 : 115,
F5 : 116,
F6 : 117,
F7 : 118,
F8 : 119,
F9 : 120,
F10 : 121,
EQUAL : 187,
HYPHEN : 189,
DOT : 190,
SLASH : 191,
TRA : 192, /* Typewritten Reverse Apostrophe (`) */
BACKSLASH : 220,
BRACKET_CLOSE: 221
};

View file

@ -1,20 +1,23 @@
/* global DOM */ /* global DOM */
import {escapeRegExp} from '#common/util';
export default function setCurrentByChar(char, charStore) { 'use strict';
const Info = DOM.CurrentInfo;
const Info = DOM.CurrentInfo;
const {escapeRegExp} = require('../../common/util');
module.exports = function setCurrentByChar(char, charStore) {
let firstByName; let firstByName;
let skipCount = 0; let skipCount = 0;
let set = false; let setted = false;
let i = 0; let i = 0;
const escapeChar = escapeRegExp(char); const escapeChar = escapeRegExp(char);
const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); const regExp = new RegExp('^' + escapeChar + '.*$', 'i');
const {files} = Info; const {files} = Info;
const chars = charStore(); const chars = charStore();
const n = chars.length; const n = chars.length;
while (i < n && char === chars[i]) while(i < n && char === chars[i])
i++; i++;
if (!i) if (!i)
@ -28,21 +31,19 @@ export default function setCurrentByChar(char, charStore) {
const isTest = (a) => regExp.test(a); const isTest = (a) => regExp.test(a);
const isRoot = (a) => a === '..'; const isRoot = (a) => a === '..';
const not = (f) => (a) => !f(a); const not = (f) => (a) => !f(a);
const setCurrent = (name) => { const setCurrent = (name) => {
const byName = DOM.getCurrentByName(name); const byName = DOM.getCurrentByName(name);
if (!skipCount) { if (!skipCount) {
set = true; setted = true;
DOM.setCurrentFile(byName); DOM.setCurrentFile(byName);
return true; return true;
} else {
if (skipN === skipCount)
firstByName = byName;
--skipCount;
} }
if (skipN === skipCount)
firstByName = byName;
--skipCount;
}; };
names names
@ -50,8 +51,9 @@ export default function setCurrentByChar(char, charStore) {
.filter(not(isRoot)) .filter(not(isRoot))
.some(setCurrent); .some(setCurrent);
if (!set) { if (!setted) {
DOM.setCurrentFile(firstByName); DOM.setCurrentFile(firstByName);
charStore([char]); charStore([char]);
} }
} };

View file

@ -1,36 +1,41 @@
import {fullstore} from 'fullstore'; 'use strict';
import limier from 'limier'; /* global DOM */
const fullstore = require('fullstore/legacy');
const limier = require('limier/legacy');
const Info = DOM.CurrentInfo;
const searchStore = fullstore([]); const searchStore = fullstore([]);
const searchIndex = fullstore(0); const searchIndex = fullstore(0);
export const find = (value, names) => { module.exports.find = (value) => {
const names = Info.files.map(DOM.getCurrentName);
const result = limier(value, names); const result = limier(value, names);
searchStore(result); searchStore(result);
searchIndex(0); searchIndex(0);
return result; DOM.setCurrentByName(result[0]);
}; };
export const findNext = () => { module.exports.findNext = () => {
const names = searchStore(); const names = searchStore();
const index = next(searchIndex(), names.length); const index = next(searchIndex(), names.length);
searchIndex(index); searchIndex(index);
return names[searchIndex()]; DOM.setCurrentByName(names[searchIndex()]);
}; };
export const findPrevious = () => { module.exports.findPrevious = () => {
const names = searchStore(); const names = searchStore();
const index = previous(searchIndex(), names.length); const index = previous(searchIndex(), names.length);
searchIndex(index); searchIndex(index);
return names[index]; DOM.setCurrentByName(names[index]);
}; };
export const _next = next; module.exports._next = next;
export const _previous = previous; module.exports._previous = previous;
function next(index, length) { function next(index, length) {
if (index === length - 1) if (index === length - 1)
@ -45,3 +50,4 @@ function previous(index, length) {
return --index; return --index;
} }

View file

@ -1,55 +0,0 @@
import test from 'supertape';
import {getDOM} from './globals.fixture.js';
import {
_next,
_previous,
find,
findNext,
findPrevious,
} from './find.js';
globalThis.DOM = getDOM();
test('cloudcmd: client: vim: _next', (t) => {
const result = _next(1, 2);
t.notOk(result, 'should return 0');
t.end();
});
test('cloudcmd: client: vim: _next: increment', (t) => {
const result = _next(0, 2);
t.equal(result, 1, 'should return 1');
t.end();
});
test('cloudcmd: client: vim: _previous', (t) => {
const result = _previous(0, 2);
t.equal(result, 1, 'should return 1');
t.end();
});
test('cloudcmd: client: vim: _previous: decrement', (t) => {
const result = _previous(1, 2);
t.equal(result, 0, 'should return 0');
t.end();
});
test('cloudcmd: client: vim: findNext: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findNext();
t.equal(result, 'beta', 'should return next found name');
t.end();
});
test('cloudcmd: client: vim: findPrevious: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findPrevious();
t.equal(result, 'apple', 'should return previous found name');
t.end();
});

View file

@ -1,167 +1,199 @@
import vim from './vim.js'; 'use strict';
import * as finder from './find.js'; /* global CloudCmd, DOM */
import {
setCurrent,
selectFileNotParent,
} from './set-current.js';
export default (key, event, overrides = {}) => { const KEY = require('../key');
const defaults = { const Info = DOM.CurrentInfo;
...globalThis.DOM, const Dialog = DOM.Dialog;
...globalThis.CloudCmd,
};
const deps = { const fullstore = require('fullstore/legacy');
...defaults, const store = fullstore('');
...overrides, const visual = fullstore(false);
}; const {
find,
findNext,
findPrevious,
} = require('./find');
const operations = getOperations(event, deps); const TITLE = 'Cloud Commander';
vim(key, operations, deps); const stopVisual = () => {
visual(false);
}; };
const getOperations = (event, deps) => { const end = () => {
const { store('');
Info = globalThis.DOM.CurrentInfo,
CloudCmd = globalThis.CloudCmd,
Operation,
unselectFiles,
setCurrentFile,
setCurrentByName,
getCurrentName,
prompt = globalThis.DOM.Dialog.prompt,
preventDefault = event?.preventDefault?.bind(event),
stopImmediatePropagation = event?.preventDefault?.bind(event),
promptNewFile = globalThis.DOM.promptNewFile,
toggleSelectedFile,
Buffer = {},
createFindNext = _createFindNext,
createFindPrevious = _createFindPrevious,
createMakeFile = _createMakeFile,
renameCurrent,
} = deps;
return {
makeFile: createMakeFile({
promptNewFile,
preventDefault,
stopImmediatePropagation,
}),
findNext: createFindNext({
setCurrentByName,
}),
findPrevious: createFindPrevious({
setCurrentByName,
}),
escape: unselectFiles,
rename: () => {
event.preventDefault();
renameCurrent();
},
remove: () => {
Operation.show('delete');
},
operationCopy: () => {
event.preventDefault();
Operation.show('copy');
},
operationMove: () => {
event.preventDefault();
Operation.show('move');
},
makeDirectory: () => {
event.stopImmediatePropagation();
event.preventDefault();
globalThis.DOM.promptNewDir();
},
terminal: () => {
CloudCmd.Terminal.show();
},
edit: () => {
CloudCmd.EditFileVim.show();
},
copy: () => {
Buffer.copy();
unselectFiles();
},
select: () => {
const current = Info.element;
toggleSelectedFile(current);
},
paste: Buffer.paste,
moveNext: ({count, isVisual, isDelete}) => {
setCurrent('next', {
count,
isVisual,
isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
});
},
movePrevious: ({count, isVisual, isDelete}) => {
setCurrent('previous', {
count,
isVisual,
isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
});
},
find: async () => {
preventDefault();
const [, value] = await prompt('Find', '');
if (!value)
return;
const names = Info.files.map(getCurrentName);
const [result] = finder.find(value, names);
setCurrentByName(result);
},
};
}; };
export const selectFile = selectFileNotParent; const rmFirst = (a) => {
return a
const _createFindPrevious = (overrides = {}) => () => { .split('')
const {setCurrentByName} = overrides; .slice(1)
const name = finder.findPrevious(); .join('');
setCurrentByName(name);
}; };
const _createFindNext = (overrides = {}) => () => { module.exports = (key, event) => {
const {setCurrentByName} = overrides; const current = Info.element;
const name = finder.findNext(); const keyCode = event.keyCode;
const prevStore = store();
setCurrentByName(name); const value = store(prevStore.concat(key));
if (keyCode === KEY.ENTER)
return end();
if (keyCode === KEY.ESC) {
DOM.unselectFiles();
visual(false);
return end();
}
if (key === 'j') {
move('next', {
prevStore,
current,
});
return end();
}
if (key === 'k') {
move('previous', {
prevStore,
current,
});
return end();
}
if (/gg/.test(value)) {
move('previous', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'd' && (visual() || prevStore === 'd')) {
CloudCmd.Operation.show('delete');
stopVisual();
return end();
}
if (key === 'G') {
move('next', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'y') {
if (!visual())
return end();
DOM.Buffer.copy();
stopVisual();
DOM.unselectFiles();
return end();
}
if (/^p$/i.test(key)) {
DOM.Buffer.paste();
return end();
}
if (/^v$/i.test(key)) {
DOM.toggleSelectedFile(current);
visual(!visual());
return end();
}
if (key === '/') {
event.preventDefault();
Dialog.prompt(TITLE, 'Find', '', {cancel: false})
.then(find);
return end();
}
if (key === 'n') {
findNext();
return end();
}
if (key === 'N') {
findPrevious();
return end();
}
}; };
const _createMakeFile = (overrides = {}) => () => { module.exports.selectFile = selectFile;
const {
promptNewFile, function move(sibling, {max, current, prevStore}) {
stopImmediatePropagation, const isDelete = prevStore[0] === 'd';
preventDefault,
} = overrides; if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const n = max || getNumber(prevStore);
if (isNaN(n))
return;
setCurrent({
n,
current,
sibling,
visual: visual(),
});
if (isDelete)
CloudCmd.Operation.show('delete');
}
function getNumber(value) {
if (!value)
return 1;
if (value === 'g')
return 1;
return parseInt(value);
}
function selectFile(current) {
const name = DOM.getCurrentName(current);
if (name === '..')
return;
DOM.selectFile(current);
}
function setCurrent({n, current, visual, sibling}) {
const select = visual ? selectFile : DOM.unselectFile;
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < n; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
DOM.setCurrentFile(current);
}
stopImmediatePropagation();
preventDefault();
promptNewFile();
};

View file

@ -1,756 +0,0 @@
import {test, stub} from 'supertape';
import {getDOM, getCloudCmd} from './globals.fixture.js';
import vim, {selectFile as vimSelectFile} from './index.js';
import * as finder from './find.js';
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {assign} = Object;
const {DOM} = globalThis;
const {Buffer} = DOM;
test('cloudcmd: client: key: set next file: no', (t) => {
const element = {};
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
};
vim('j', {}, {
Info,
setCurrentFile,
unselectFiles,
});
t.calledWith(setCurrentFile, [element], 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current: j', async (t) => {
const nextSibling = 'hello';
const element = {
nextSibling,
};
const setCurrentFile = stub();
const Info = {
element,
};
await vim('j', {}, {
Info,
setCurrentFile,
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current: mjj', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling,
};
const setCurrentFile = stub();
const Info = {
element,
};
const deps = {
Info,
setCurrentFile,
unselectFiles: stub(),
};
vim('m', {}, deps);
vim('j', {}, deps);
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current: g', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling,
};
const setCurrentFile = stub();
const Info = {
element,
};
const deps = {
Info,
setCurrentFile,
unselectFiles: stub(),
};
vim('g', {}, deps);
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should ignore g');
t.end();
});
test('cloudcmd: client: key: set +2 file current', (t) => {
const last = {};
const setCurrentFile = stub();
const element = {};
const Info = {
element,
};
const deps = {
setCurrentFile,
Info,
unselectFiles: stub(),
};
const event = {};
vim('2', event, deps);
vim('j', event, deps);
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end();
});
test('cloudcmd: client: key: select +2 files from current before delete', (t) => {
const last = {};
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
const Info = {
element,
};
const Operation = {
show: stub(),
};
const selectFile = stub();
const getCurrentName = stub().returns('x');
const event = {};
const deps = {
Info,
setCurrentFile,
selectFile,
getCurrentName,
Operation,
};
vim('d', event, deps);
vim('2', event, deps);
vim('j', event, deps);
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end();
});
test('cloudcmd: client: key: delete +2 files from current', (t) => {
const last = {};
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
const show = stub();
const deps = {
Info: {
element,
},
Operation: {
show,
},
setCurrentFile,
selectFile: stub(),
getCurrentName: stub().returns('x'),
unselectFiles: stub(),
};
const event = {};
vim('d', event, deps);
vim('2', event, deps);
vim('j', event, deps);
t.calledWith(show, ['delete'], 'should call delete');
t.end();
});
test('cloudcmd: client: key: set previous file current', (t) => {
const previousSibling = 'hello';
const element = {
previousSibling,
};
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
};
const deps = {
Info,
setCurrentFile,
unselectFiles,
};
vim('k', {}, deps);
t.calledWith(setCurrentFile, [previousSibling], 'should set previous file');
t.end();
});
test('cloudcmd: client: key: copy: no', (t) => {
const copy = stub();
vim('y', {}, {
unselectFiles: stub(),
Buffer: {
copy,
},
});
t.notCalled(copy, 'should not copy files');
t.end();
});
test('cloudcmd: client: key: copy', (t) => {
const copy = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub();
const unselectFiles = stub();
const deps = {
Info,
unselectFiles,
toggleSelectedFile,
Buffer: {
copy,
},
};
vim('v', {}, deps);
vim('y', {}, deps);
t.calledWithNoArgs(copy, 'should copy files');
t.end();
});
test('cloudcmd: client: key: copy: unselectFiles', (t) => {
const unselectFiles = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub();
const deps = {
Info,
unselectFiles,
toggleSelectedFile,
Buffer: {
copy: stub(),
},
};
vim('v', {}, deps);
vim('y', {}, deps);
t.calledWithNoArgs(unselectFiles, 'should unselect files');
t.end();
});
test('cloudcmd: client: key: paste', (t) => {
const paste = stub();
Buffer.paste = paste;
vim('p', {}, {
Buffer,
});
t.calledWithNoArgs(paste, 'should paste files');
t.end();
});
test('cloudcmd: client: key: selectFile: ..', (t) => {
const getCurrentName = stub().returns('..');
const selectFile = stub();
const current = {};
vimSelectFile(current, {
selectFile,
getCurrentName,
});
t.notCalled(selectFile, 'should not call selectFile');
t.end();
});
test('cloudcmd: client: key: selectFile', (t) => {
const selectFile = stub();
const getCurrentName = stub().returns('x');
const current = {};
vimSelectFile(current, {
selectFile,
getCurrentName,
});
t.calledWith(selectFile, [current], 'should call selectFile');
t.end();
});
test('cloudcmd: client: key: set last file current: shift + g', async (t) => {
const last = 'last';
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
await vim('G', {}, {
Info: {
element,
},
setCurrentFile,
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end();
});
test('cloudcmd: client: key: set last file current: $', (t) => {
const last = 'last';
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
vim('$', {}, {
Info: {
element,
},
setCurrentFile,
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end();
});
test('cloudcmd: client: key: set first file current: gg', (t) => {
const first = 'first';
const previousSibling = {
previousSibling: first,
};
const element = {
previousSibling,
};
const Operation = {
show: stub(),
};
const unselectFiles = stub();
const setCurrentFile = stub();
const deps = {
Operation,
unselectFiles,
setCurrentFile,
Info: {
element,
},
};
vim('g', {}, deps);
vim('g', {}, deps);
t.calledWith(setCurrentFile, [first], 'should set first file');
t.end();
});
test('cloudcmd: client: key: set first file current: ^', async (t) => {
const first = 'first';
const previousSibling = {
previousSibling: first,
};
const element = {
previousSibling,
};
const Operation = {
show: stub(),
};
const unselectFiles = stub();
const setCurrentFile = stub();
const deps = {
setCurrentFile,
Info: {
element,
},
unselectFiles,
Operation,
};
await vim('^', {}, deps);
t.calledWith(setCurrentFile, [first], 'should set first file');
t.end();
});
test('cloudcmd: client: key: visual', (t) => {
const element = {};
const toggleSelectedFile = stub();
const Info = {
element,
};
vim('v', {}, {
Info,
toggleSelectedFile,
});
t.calledWith(toggleSelectedFile, [element], 'should toggle selection');
t.end();
});
test('cloudcmd: client: key: ESC', (t) => {
const element = {};
const unselectFiles = stub();
const Info = {
element,
};
vim('Escape', null, {
Info,
unselectFiles,
});
t.calledWithNoArgs(unselectFiles, 'should toggle selection');
t.end();
});
test('cloudcmd: client: key: Enter', async (t) => {
const nextSibling = 'hello';
const element = {
nextSibling,
};
const unselectFiles = stub();
const setCurrentFile = stub();
const Info = {
element,
};
await vim('Enter', null, {
Info,
setCurrentFile,
unselectFiles,
});
await vim('j', null, {
Info,
setCurrentFile,
unselectFiles,
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end();
});
test('cloudcmd: client: key: /', (t) => {
const preventDefault = stub();
const element = {};
const Info = {
element,
files: [],
};
const getCurrentName = stub().returns('');
const event = {
preventDefault,
};
const prompt = stub().returns([]);
vim('/', event, {
getCurrentName,
Info,
prompt,
});
t.calledWithNoArgs(preventDefault);
t.end();
});
test('cloudcmd: client: find', (t) => {
assign(DOM.Dialog, {
prompt: stub().returns([]),
});
const setCurrentByName = stub();
assign(DOM, {
setCurrentByName,
});
const event = {
preventDefault: stub(),
};
vim('/', event);
t.notCalled(setCurrentByName);
t.end();
});
test('cloudcmd: client: key: n', (t) => {
const findNext = stub();
const createFindNext = stub().returns(findNext);
const event = {};
vim('n', event, {
createFindNext,
});
t.calledWithNoArgs(findNext, 'should call findNext');
t.end();
});
test('cloudcmd: client: key: N', (t) => {
const findPrevious = stub();
const createFindPrevious = stub().returns(findPrevious);
const event = {};
vim('N', event, {
createFindPrevious,
});
t.calledWithNoArgs(findPrevious);
t.end();
});
test('cloudcmd: client: key: make directory', async (t) => {
const {DOM} = globalThis;
assign(DOM, {
promptNewDir: stub(),
});
const event = {
stopImmediatePropagation: stub(),
preventDefault: stub(),
};
await vim('m', event);
await vim('d', event);
t.calledWithNoArgs(DOM.promptNewDir);
t.end();
});
test('cloudcmd: client: key: make file', (t) => {
const promptNewFile = stub();
const event = {
stopImmediatePropagation: stub(),
preventDefault: stub(),
};
vim('m', event);
vim('f', event, {
promptNewFile,
});
t.calledWithNoArgs(promptNewFile);
t.end();
});
test('cloudcmd: client: vim: terminal', (t) => {
const CloudCmd = {
Terminal: {
show: stub(),
},
};
const event = {};
vim('t', event, {
CloudCmd,
});
vim('t', event, {
CloudCmd,
});
t.calledWithNoArgs(CloudCmd.Terminal.show);
t.end();
});
test('cloudcmd: client: vim: edit', async (t) => {
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {CloudCmd} = globalThis;
assign(CloudCmd, {
EditFileVim: {
show: stub(),
},
});
const event = {};
await vim('e', event);
t.calledWithNoArgs(CloudCmd.EditFileVim.show);
t.end();
});
test('cloudcmd: client: vim: rename', async (t) => {
const DOM = getDOM();
const renameCurrent = stub();
assign(DOM, {
renameCurrent,
});
const event = {
preventDefault: stub(),
};
await vim('rr', event, DOM);
t.calledWithNoArgs(renameCurrent);
t.end();
});
test('cloudcmd: client: key: cc: operationCopy', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('c', event, {
Operation,
});
vim('c', event, {
Operation,
});
t.calledWith(show, ['copy'], 'should show copy operation');
t.end();
});
test('cloudcmd: client: key: mm: operationMove', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('m', event, {
Operation,
});
vim('m', event, {
Operation,
});
t.calledWith(show, ['move'], 'should show move operation');
t.end();
});
test('cloudcmd: client: key: n: findNext: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('n', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['beta'], 'should set current by next found name');
t.end();
});
test('cloudcmd: client: key: N: findPrevious: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('N', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['apple'], 'should set current by previous found name');
t.end();
});

View file

@ -1,33 +0,0 @@
/* global DOM */
export function selectFileNotParent(current, {getCurrentName, selectFile} = DOM) {
const name = getCurrentName(current);
if (name === '..')
return;
selectFile(current);
}
export const setCurrent = (sibling, {count, isVisual, isDelete}, {Info, setCurrentFile, unselectFiles, Operation}) => {
let current = Info.element;
const select = isVisual ? selectFileNotParent : unselectFiles;
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < count; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
setCurrentFile(current);
if (isDelete)
Operation.show('delete');
};

View file

@ -1,220 +0,0 @@
import {fullstore} from 'fullstore';
const store = fullstore('');
const visual = fullstore(false);
const stopVisual = () => {
visual(false);
};
const end = () => {
store('');
};
const rmFirst = (a) => {
return a
.split('')
.slice(1)
.join('');
};
const noop = () => {};
export default (key, operations = {}) => {
const prevStore = store();
const isVisual = visual();
const value = store(prevStore.concat(key));
const {
escape = noop,
moveNext = noop,
movePrevious = noop,
remove = noop,
copy = noop,
paste = noop,
select = noop,
find = noop,
findNext = noop,
findPrevious = noop,
makeFile = noop,
makeDirectory = noop,
terminal = noop,
edit = noop,
operationCopy = noop,
operationMove = noop,
rename = noop,
} = operations;
if (key === 'Enter')
return end();
if (key === 'Escape') {
visual(false);
escape();
return end();
}
if (key === 'j' || key === 'w') {
const {
count,
isDelete,
isVisual,
} = handleDelete(prevStore);
!isNaN(count) && moveNext({
count,
isVisual,
isDelete,
});
return end();
}
if (key === 'k' || key === 'b') {
const {
count,
isDelete,
isVisual,
} = handleDelete(prevStore);
!isNaN(count) && movePrevious({
count,
isVisual,
isDelete,
});
return end();
}
if (value === 'gg' || key === '^') {
const {isDelete, isVisual} = handleDelete(prevStore);
movePrevious({
count: Infinity,
isVisual,
isDelete,
});
return end();
}
if (value === 'md') {
makeDirectory();
return end();
}
if (value === 'tt') {
terminal();
return end();
}
if (value === 'e') {
edit();
return end();
}
if (value === 'cc') {
operationCopy();
return end();
}
if (value === 'mm') {
operationMove();
return end();
}
if (value === 'mf') {
makeFile();
return end();
}
if (value === 'rr') {
rename();
return end();
}
if (key === 'd' && (visual() || prevStore === 'd')) {
stopVisual();
remove();
return end();
}
if (key === 'G' || key === '$') {
moveNext({
count: Infinity,
isVisual,
});
return end();
}
if (key === 'y') {
if (!visual())
return end();
stopVisual();
copy();
return end();
}
if (/^p$/i.test(key)) {
paste();
return end();
}
if (/^v$/i.test(key)) {
visual(!visual());
select();
return end();
}
if (key === '/') {
find();
return end();
}
if (key === 'n') {
findNext();
return end();
}
if (key === 'N') {
findPrevious();
return end();
}
if (key === ' ')
return end();
};
function handleDelete(prevStore) {
const isDelete = prevStore[0] === 'd';
if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const count = getNumber(prevStore);
const isVisual = visual();
return {
count,
isDelete,
isVisual,
};
}
function getNumber(value) {
if (!value)
return 1;
if (value === 'g')
return 1;
return parseInt(value);
}

View file

@ -1,100 +0,0 @@
import {test, stub} from 'supertape';
import vim from './vim.js';
test('vim: no operations', (t) => {
const result = vim('hello', {});
t.notOk(result);
t.end();
});
test('vim: space', (t) => {
const moveNext = stub();
vim(' ');
vim('j', {
moveNext,
});
const args = [{
count: 1,
isDelete: false,
isVisual: false,
}];
t.calledWith(moveNext, args);
t.end();
});
test('vim: ^', (t) => {
const movePrevious = stub();
vim('^', {
movePrevious,
});
const expected = {
count: Infinity,
isVisual: false,
isDelete: false,
};
t.calledWith(movePrevious, [expected], 'should call movePrevious');
t.end();
});
test('vim: cc', (t) => {
const operationCopy = stub();
vim('cc', {
operationCopy,
});
t.calledWithNoArgs(operationCopy);
t.end();
});
test('vim: mm', (t) => {
const operationMove = stub();
vim('mm', {
operationMove,
});
t.calledWithNoArgs(operationMove);
t.end();
});
test('vim: w', (t) => {
const moveNext = stub();
vim('w', {
moveNext,
});
const expected = {
count: 1,
isVisual: false,
isDelete: false,
};
t.calledWith(moveNext, [expected], 'should call moveNext');
t.end();
});
test('vim: b', (t) => {
const movePrevious = stub();
vim('b', {
movePrevious,
});
const expected = {
count: 1,
isVisual: false,
isDelete: false,
};
t.calledWith(movePrevious, [expected], 'should call movePrevious');
t.end();
});

View file

@ -1,10 +1,11 @@
import currify from 'currify'; 'use strict';
export const getIndex = currify((array, item) => { module.exports = (array, item) => {
const index = array.indexOf(item); const index = array.indexOf(item);
if (!~index) if (!~index)
return 0; return 0;
return index; return index;
}); };

View file

@ -1,4 +1,6 @@
export const getRange = (indexFrom, indexTo, files) => { 'use strict';
module.exports = (indexFrom, indexTo, files) => {
if (indexFrom < indexTo) if (indexFrom < indexTo)
return files.slice(indexFrom, indexTo + 1); return files.slice(indexFrom, indexTo + 1);
@ -7,3 +9,4 @@ export const getRange = (indexFrom, indexTo, files) => {
return [files[indexFrom]]; return [files[indexFrom]];
}; };

View file

@ -1,52 +0,0 @@
import test from 'supertape';
import {getRange} from './get-range.js';
test('cloudcmd: client: listeners: getRange: direct', (t) => {
const expected = [
'hello',
'world',
];
const files = [
...expected,
'how',
'come',
];
const result = getRange(0, 1, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});
test('cloudcmd: client: listeners: getRange: reverse', (t) => {
const expected = [
'hello',
'world',
];
const files = [
...expected,
'how',
'come',
];
const result = getRange(1, 0, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});
test('cloudcmd: client: listeners: getRange: one', (t) => {
const expected = ['hello'];
const files = [
...expected,
'how',
'come',
];
const result = getRange(0, 0, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});

View file

@ -1,30 +1,38 @@
/* global DOM, CloudCmd */ /* global DOM, CloudCmd */
import exec from 'execon';
import itype from 'itype'; 'use strict';
import currify from 'currify';
import {tryToCatch} from 'try-to-catch'; const exec = require('execon');
import clipboard from '@cloudcmd/clipboard'; const itype = require('itype/legacy');
import * as Events from '#dom/events'; const currify = require('currify/legacy');
import {uploadFiles} from '#dom/upload-files'; const getRange = require('./get-range');
import {FS} from '#common/cloudfunc';
import {getRange} from './get-range.js'; const getIndex = currify(require('./get-index'));
import {getIndex} from './get-index.js';
const uploadFiles = require('../dom/upload-files');
const {
FS,
apiURL
} = require('../../common/cloudfunc');
const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const SPACE = ' '; const SPACE = ' ';
export async function init() { module.exports.init = () => {
contextMenu(); contextMenu();
dragndrop(); dragndrop();
unload(); unload();
pop(); pop();
resize(); resize();
config();
header(); header();
await config(); };
}
CloudCmd.Listeners = module.exports;
const unselect = (event) => { const unselect = (event) => {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
const { const {
shiftKey, shiftKey,
metaKey, metaKey,
@ -38,16 +46,23 @@ const unselect = (event) => {
}; };
const execAll = currify((funcs, event) => { const execAll = currify((funcs, event) => {
for (const fn of funcs) funcs.forEach((fn) => {
fn(event); fn(event);
});
}); });
const Info = DOM.CurrentInfo;
const Storage = DOM.Storage;
const Events = DOM.Events;
const EventsFiles = { const EventsFiles = {
mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), mousedown: exec.with(execIfNotUL, setCurrentFileByEvent),
click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), click: execAll([
onClick,
unselect,
]),
dragstart: exec.with(execIfNotUL, onDragStart), dragstart: exec.with(execIfNotUL, onDragStart),
dblclick: exec.with(execIfNotUL, onDblClick), dblclick: exec.with(execIfNotUL, onDblClick),
touchstart: exec.with(execIfNotUL, onTouch), touchstart: exec.with(execIfNotUL, onTouch)
}; };
let EXT; let EXT;
@ -55,7 +70,6 @@ let EXT;
function header() { function header() {
const fm = DOM.getFM(); const fm = DOM.getFM();
const isDataset = (el) => el.dataset; const isDataset = (el) => el.dataset;
const isPanel = (el) => { const isPanel = (el) => {
return /^js-(left|right)$/.test(el.dataset.name); return /^js-(left|right)$/.test(el.dataset.name);
}; };
@ -67,7 +81,8 @@ function header() {
if (parent.dataset.name !== 'js-fm-header') if (parent.dataset.name !== 'js-fm-header')
return; return;
const name = (el.dataset.name || '').replace('js-', ''); const name = (el.dataset.name || '')
.replace('js-', '');
if (!/^(name|size|date)$/.test(name)) if (!/^(name|size|date)$/.test(name))
return; return;
@ -88,46 +103,42 @@ function getPath(el, path = []) {
return getPath(el.parentElement, path.concat(el)); return getPath(el.parentElement, path.concat(el));
} }
async function config() { function config() {
const [, config] = await tryToCatch(DOM.Files.get, 'config'); DOM.Files.get('config', (e, config) => {
const type = config?.packer; const type = config && config.packer;
EXT = DOM.getPackerExt(type);
EXT = DOM.getPackerExt(type); });
} }
export const initKeysPanel = () => { module.exports.initKeysPanel = () => {
const keysElement = DOM.getById('js-keyspanel'); const keysElement = DOM.getById('js-keyspanel');
if (!keysElement) if (!keysElement)
return; return;
Events.addClick(keysElement, (event) => { Events.addClick(keysElement, ({target}) => {
const {target} = event; const id = target.id;
const {id} = target;
const operation = (name) => { const operation = (name) => {
const {Operation} = CloudCmd; const Operation = CloudCmd.Operation;
const fn = Operation.show.bind(null, name);
return Operation.show.bind(null, name); return fn;
}; };
const clickFuncs = { const clickFuncs = {
'f1': CloudCmd.Help.show, 'f1' : CloudCmd.Help.show,
'f2': CloudCmd.UserMenu.show, 'f2' : DOM.renameCurrent,
'f3': CloudCmd.View.show, 'f3' : CloudCmd.View.show,
'f4': CloudCmd.EditFile.show, 'f4' : CloudCmd.EditFile.show,
'f5': operation('copy'), 'f5' : operation('copy'),
'f6': operation('move'), 'f6' : operation('move'),
'f7': DOM.promptNewDir, 'f7' : DOM.promptNewDir,
'f8': operation('delete'), 'f8' : operation('delete'),
'f9': () => { 'f9' : CloudCmd.Menu.show,
event.stopPropagation(); 'f10' : CloudCmd.Config.show,
CloudCmd.Menu.show(); '~' : CloudCmd.Konsole.show,
}, 'shift~' : CloudCmd.Terminal.show,
'f10': CloudCmd.Config.show, 'contact' : CloudCmd.Contact.show,
'~': CloudCmd.Konsole.show,
'shift~': CloudCmd.Terminal.show,
'contact': CloudCmd.Contact.show,
}; };
exec(clickFuncs[id]); exec(clickFuncs[id]);
@ -138,10 +149,10 @@ const getPanel = (side) => {
if (!itype.string(side)) if (!itype.string(side))
return side; return side;
return DOM.getByDataName(`js-${side}`); return DOM.getByDataName('js-' + side);
}; };
export const setOnPanel = (side) => { module.exports.setOnPanel = (side) => {
const panel = getPanel(side); const panel = getPanel(side);
const filesElement = DOM.getByDataName('js-files', panel); const filesElement = DOM.getByDataName('js-files', panel);
@ -157,45 +168,41 @@ function getPathListener(panel) {
} }
function isNoCurrent(panel) { function isNoCurrent(panel) {
const Info = DOM.CurrentInfo;
const infoPanel = Info.panel; const infoPanel = Info.panel;
if (!infoPanel)
return true;
const namePanel = panel.getAttribute('data-name'); const namePanel = panel.getAttribute('data-name');
const nameInfoPanel = infoPanel.getAttribute('data-name'); const nameInfoPanel = infoPanel.getAttribute('data-name');
return namePanel !== nameInfoPanel; return namePanel !== nameInfoPanel;
} }
function decodePath(path) { function decodePath(path){
const url = CloudCmd.HOST; const url = CloudCmd.HOST;
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
const prefixReg = RegExp('^' + prefix + FS); const prefixReg = RegExp('^' + prefix + FS);
return decodeURI(path) return decodeURI(path)
.replace(url, '') .replace(url, '')
.replace(prefixReg, '') // browser doesn't replace % -> %25% do it for him .replace(prefixReg, '')
// browser doesn't replace % -> %25% do it for him
.replace('%%', '%25%') .replace('%%', '%25%')
.replace(NBSP_REG, SPACE) || '/'; .replace(NBSP_REG, SPACE) || '/';
} }
async function onPathElementClick(panel, event) {
const Info = DOM.CurrentInfo; function onPathElementClick(panel, event) {
event.preventDefault(); event.preventDefault();
const element = event.target; const element = event.target;
const attr = element.getAttribute('data-name'); const attr = element.getAttribute('data-name');
const noCurrent = isNoCurrent(panel); const noCurrent = isNoCurrent(panel);
if (attr === 'js-copy-path') if (attr === 'js-clear-storage')
return copyPath(element); return Storage.clear();
if (attr === 'js-refresh') if (attr === 'js-refresh')
return CloudCmd.refresh({ return CloudCmd.refresh({
panel, panel,
noCurrent, noCurrent
}); });
if (attr !== 'js-path-link') if (attr !== 'js-path-link')
@ -204,26 +211,13 @@ async function onPathElementClick(panel, event) {
const {href} = element; const {href} = element;
const path = decodePath(href); const path = decodePath(href);
await CloudCmd.changeDir(path, { CloudCmd.loadDir({
path,
isRefresh: false, isRefresh: false,
panel: noCurrent ? panel : Info.panel, panel: noCurrent ? panel : Info.panel
}); });
} }
function copyPath(el) {
clipboard
.writeText(el.parentElement.title)
.then(CloudCmd.log)
.catch(CloudCmd.log);
}
function execIfNotMobile(callback, event) {
const isMobile = DOM.getCSSVar('is-mobile');
if (!isMobile)
callback(event);
}
function execIfNotUL(callback, event) { function execIfNotUL(callback, event) {
const {target} = event; const {target} = event;
const {tagName} = target; const {tagName} = target;
@ -233,27 +227,30 @@ function execIfNotUL(callback, event) {
} }
function onClick(event) { function onClick(event) {
event.preventDefault(); const ctrl = event.ctrlKey;
if (!ctrl)
event.preventDefault();
changePanel(event.target); changePanel(event.target);
} }
function toggleSelect(key, files) { function toggleSelect(key, files) {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
if (!key) if (!key)
throw Error('key should not be undefined!'); throw Error('key should not be undefined!');
const [file] = files; const [file] = files;
if (isMac && key.meta) if (isMac && key.meta || key.ctrl)
return DOM.toggleSelectedFile(file); return DOM.toggleSelectedFile(file);
if (key.shift) if (key.shift)
return files.map(DOM.selectFile); return files.forEach(DOM.selectFile);
} }
function changePanel(element) { function changePanel(element) {
const Info = DOM.CurrentInfo;
const {panel} = Info; const {panel} = Info;
const files = DOM.getByDataName('js-files', panel); const files = DOM.getByDataName('js-files', panel);
const ul = getULElement(element); const ul = getULElement(element);
@ -262,20 +259,21 @@ function changePanel(element) {
DOM.changePanel(); DOM.changePanel();
} }
async function onDblClick(event) { function onDblClick(event) {
event.preventDefault();
const current = getLIElement(event.target); const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current); const isDir = DOM.isCurrentIsDir(current);
const path = DOM.getCurrentPath(current); const path = DOM.getCurrentPath(current);
if (!isDir) if (isDir) {
return CloudCmd.View.show(); CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/'
});
await CloudCmd.changeDir(path); event.preventDefault();
}
} }
async function onTouch(event) { function onTouch(event) {
const current = getLIElement(event.target); const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current); const isDir = DOM.isCurrentIsDir(current);
@ -287,7 +285,11 @@ async function onTouch(event) {
if (!isCurrent) if (!isCurrent)
return; return;
await CloudCmd.changeDir(DOM.getCurrentPath(current)); CloudCmd.loadDir({
path: DOM.getCurrentPath(current)
});
event.preventDefault();
} }
/* /*
@ -295,10 +297,8 @@ async function onTouch(event) {
* in Chrome (HTML5) * in Chrome (HTML5)
*/ */
function onDragStart(event) { function onDragStart(event) {
const Info = DOM.CurrentInfo;
const {prefixURL} = CloudCmd;
const element = getLIElement(event.target); const element = getLIElement(event.target);
const {isDir} = Info; const isDir = Info.isDir;
let link = DOM.getCurrentLink(element); let link = DOM.getCurrentLink(element);
let name = DOM.getCurrentName(element); let name = DOM.getCurrentName(element);
@ -307,15 +307,13 @@ function onDragStart(event) {
name += EXT; name += EXT;
link = document.createElement('a'); link = document.createElement('a');
link.textContent = name; link.textContent = name;
link.href = prefixURL + '/pack' + Info.path + EXT; link.href = apiURL + '/pack' + Info.path + EXT;
} }
event.dataTransfer.setData( event.dataTransfer.setData('DownloadURL',
'DownloadURL', 'application/octet-stream' + ':' +
'application/octet-stream' + ':' + name + name + ':' +
':' + link);
link,
);
} }
function getLIElement(element) { function getLIElement(element) {
@ -336,14 +334,13 @@ function getULElement(element) {
} }
function setCurrentFileByEvent(event) { function setCurrentFileByEvent(event) {
const Info = DOM.CurrentInfo;
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const key = { const key = {
alt: event.altKey, alt: event.altKey,
ctrl: event.ctrlKey, ctrl: event.ctrlKey,
meta: event.metaKey, meta: event.metaKey,
shift: event.shiftKey, shift: event.shiftKey
}; };
const element = getLIElement(event.target); const element = getLIElement(event.target);
@ -380,7 +377,7 @@ function contextMenu() {
Events.addOnce('contextmenu', fm, (event) => { Events.addOnce('contextmenu', fm, (event) => {
CloudCmd.Menu.show({ CloudCmd.Menu.show({
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY
}); });
}); });
@ -391,17 +388,21 @@ function contextMenu() {
function dragndrop() { function dragndrop() {
const panels = DOM.getByClassAll('panel'); const panels = DOM.getByClassAll('panel');
const select = ({target}) => { const select = () => {
target.classList.add('selected-panel'); [...panels].forEach((panel) => {
panel.classList.add('selected-panel');
});
}; };
const unselect = ({target}) => { const unselect = () => {
target.classList.remove('selected-panel'); [...panels].forEach((panel) => {
panel.classList.remove('selected-panel');
});
}; };
const onDrop = (event) => { const onDrop = (event) => {
const {files, items} = event.dataTransfer; const files = event.dataTransfer.files;
const items = event.dataTransfer.items;
const {length: filesCount} = files; const {length: filesCount} = files;
event.preventDefault(); event.preventDefault();
@ -410,10 +411,7 @@ function dragndrop() {
return uploadFiles(files); return uploadFiles(files);
const isFile = (item) => item.kind === 'file'; const isFile = (item) => item.kind === 'file';
const dirFiles = [...items].filter(isFile);
const dirFiles = Array
.from(items)
.filter(isFile);
if (dirFiles.length) if (dirFiles.length)
return DOM.uploadDirectory(dirFiles); return DOM.uploadDirectory(dirFiles);
@ -425,13 +423,13 @@ function dragndrop() {
}; };
/** /**
* In macOS Chrome dropEffect = 'none' * In Mac OS Chrome dropEffect = 'none'
* so drop do not firing up when try * so drop do not firing up when try
* to upload file from download bar * to upload file from download bar
*/ */
const onDragOver = (event) => { const onDragOver = (event) => {
const {dataTransfer} = event; const dataTransfer = event.dataTransfer;
const {effectAllowed} = dataTransfer; const effectAllowed = dataTransfer.effectAllowed;
if (/move|linkMove/.test(effectAllowed)) if (/move|linkMove/.test(effectAllowed))
dataTransfer.dropEffect = 'move'; dataTransfer.dropEffect = 'move';
@ -441,18 +439,19 @@ function dragndrop() {
event.preventDefault(); event.preventDefault();
}; };
for (const panel of panels) Events.add('dragenter', select);
Events Events.add(['dragleave', 'drop'], unselect);
.add('dragover', panel, onDragOver)
.add('drop', panel, onDrop) [...panels].forEach((panel) => {
.add('dragenter', select) Events.add('dragover', panel, onDragOver)
.add(['dragleave', 'drop'], unselect); .add('drop', panel, onDrop);
});
} }
function unload() { function unload() {
Events.add(['unload', 'beforeunload'], (event) => { DOM.Events.add(['unload', 'beforeunload'], (event) => {
const {Key} = CloudCmd; const Key = CloudCmd.Key;
const isBind = Key?.isBind(); const isBind = Key && Key.isBind();
if (isBind) if (isBind)
return; return;
@ -463,15 +462,15 @@ function unload() {
} }
function pop() { function pop() {
Events.add('popstate', async ({state}) => { Events.add('popstate', ({state}) => {
const path = (state || '').replace(FS, ''); const path = (state || '').replace(FS, '');
if (!path) if (!path)
return CloudCmd.route(location.hash); return CloudCmd.route(location.hash);
const history = false; const history = false;
CloudCmd.loadDir({
await CloudCmd.changeDir(path, { path,
history, history,
}); });
}); });
@ -479,8 +478,7 @@ function pop() {
function resize() { function resize() {
Events.add('resize', () => { Events.add('resize', () => {
const Info = DOM.CurrentInfo; const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
if (!is) if (!is)
return; return;
@ -500,3 +498,4 @@ function resize() {
DOM.changePanel(); DOM.changePanel();
}); });
} }

View file

@ -1,56 +0,0 @@
/* global CloudCmd */
import exec from 'execon';
import {tryToCatch} from 'try-to-catch';
import {js as loadJS} from 'load.js';
import pascalCase from 'just-pascal-case';
const noJS = (a) => a.replace(/.js$/, '');
/**
* function load modules
* @params = {name, path, func, dobefore, arg}
*/
export const loadModule = (params) => {
if (!params)
return;
const {path} = params;
const name = path && noJS(pascalCase(path));
const doBefore = params.dobefore;
if (CloudCmd[name])
return;
CloudCmd[name] = async () => {
exec(doBefore);
const {DIR_MODULES} = CloudCmd;
const pathFull = `${DIR_MODULES}/${path}.js`;
await loadJS(pathFull);
const newModule = async (f) => f && f();
const module = CloudCmd[name];
Object.assign(newModule, module);
CloudCmd[name] = newModule;
CloudCmd.log('init', name);
await module.init();
return newModule;
};
CloudCmd[name].show = async (...args) => {
CloudCmd.log('show', name, args);
const m = CloudCmd[name];
const [e, a] = await tryToCatch(m);
if (e)
return;
return await a.show(...args);
};
};

View file

@ -1,32 +1,27 @@
/* global CloudCmd, filepicker */ /* global CloudCmd, filepicker */
import exec from 'execon';
import currify from 'currify'; 'use strict';
import load from 'load.js';
import {ajax} from '#dom/load'; CloudCmd.Cloud = CloudProto;
import * as Files from '#dom/files';
import * as Images from '#dom/images'; const exec = require('execon');
const currify = require('currify/legacy');
const {log} = CloudCmd; const {log} = CloudCmd;
const load = require('../dom/load');
const Files = require('../dom/files');
const Images = require('../dom/images');
const upload = currify(_upload); const upload = currify(_upload);
const Name = 'Cloud'; function CloudProto(callback) {
loadFiles(callback);
CloudCmd[Name] = { return module.exports;
init,
uploadFile,
saveFile,
};
export async function init() {
const [modules] = await loadFiles();
const {key} = modules.data.FilePicker;
filepicker.setKey(key);
Images.hide();
} }
export function uploadFile(filename, data) { module.exports.uploadFile = (filename, data) => {
const mimetype = ''; const mimetype = '';
filepicker.store(data, { filepicker.store(data, {
@ -35,30 +30,40 @@ export function uploadFile(filename, data) {
}, (fpFile) => { }, (fpFile) => {
filepicker.exportFile(fpFile, log, log); filepicker.exportFile(fpFile, log, log);
}); });
} };
export function saveFile(callback) { module.exports.saveFile = (callback) => {
filepicker.pick(upload(callback)); filepicker.pick(upload(callback));
} };
function _upload(callback, file) { function _upload(callback, file) {
const {url, filename} = file; const {
url,
filename,
} = file;
const responseType = 'arraybuffer'; const responseType = 'arraybuffer';
const success = exec.with(callback, filename); const success = exec.with(callback, filename);
ajax({ load.ajax({
url, url,
responseType, responseType,
success, success,
}); });
} }
function loadFiles() { function loadFiles(callback) {
const js = '//api.filepicker.io/v2/filepicker.js'; const js = '//api.filepicker.io/v2/filepicker.js';
return Promise.all([ load.js(js, () => {
Files.get('modules'), Files.get('modules', (error, modules) => {
load.js(js), const {key} = modules.data.FilePicker;
]);
filepicker.setKey(key);
Images.hide();
exec(callback);
});
});
} }

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