feature: dark theme: add (#332)

This commit is contained in:
coderaiser 2024-03-29 13:25:23 +02:00
parent 35622082a9
commit 6bc4f3ec26
23 changed files with 186 additions and 51 deletions

View file

@ -1,5 +1,6 @@
'use strict';
const {env} = require('node:process');
const fs = require('node:fs');
const {
basename,
@ -10,7 +11,6 @@ const {
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const {env} = require('node:process');
const isDev = env.NODE_ENV === 'development';
const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`);
@ -23,6 +23,7 @@ const cssNames = [
'terminal',
'user-menu',
...getCSSList('columns'),
...getCSSList('themes'),
];
const cssPlugins = cssNames.map(extractCSS);
@ -36,7 +37,7 @@ const plugins = clean([
const rules = [{
test: /\.css$/,
exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/,
exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*|themes.*)\.css/,
use: extractMain.extract(['css-loader']),
}, ...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,

View file

@ -1,7 +1,7 @@
'use strict';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {env} = require('node:process');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDev = env.NODE_ENV === 'development';

View file

@ -93,7 +93,7 @@ Cloud Commander supports the following command-line parameters:
| `--terminal-command` | set command to run in terminal (shell by default)
| `--terminal-auto-restart` | restart command on exit
| `--vim` | enable vim hot keys
| `--columns` | set visible columns
| `--themes` | set visible themes
| `--export` | enable export of config through a server
| `--export-token` | authorization token used by export server
| `--import` | enable import of config
@ -122,7 +122,7 @@ Cloud Commander supports the following command-line parameters:
| `--no-terminal-command` | set default shell to run in terminal
| `--no-terminal-auto-restart` | do not restart command on exit
| `--no-vim` | disable vim hot keys
| `--no-columns` | set default visible columns
| `--no-themes` | set default visible themes
| `--no-export` | disable export config through a server
| `--no-import` | disable import of config
| `--no-import-listen` | disable listen on config updates from import server
@ -408,7 +408,7 @@ Here's a description of all options:
"terminalCommand": "", // set command to run in terminal
"terminalAutoRestart": true, // restart command on exit
"vim": false, // disable vim hot keys
"columns": "name-size-date-owner-mode", // set visible columns
"themes": "name-size-date-owner-mode", // set visible themes
"export": false, // enable export of config through a server
"exportToken": "root", // token used by export server
"import": false, // enable import of config
@ -428,7 +428,7 @@ Some config options can be overridden with environment variables, such as:
- `CLOUDCMD_NAME` - set tab name in web browser
- `CLOUDCMD_OPEN` - open web browser when server started
- `CLOUDCMD_EDITOR` - set editor
- `CLOUDCMD_COLUMNS` - set visible columns
- `CLOUDCMD_COLUMNS` - set visible themes
- `CLOUDCMD_CONTACT` - enable contact
- `CLOUDCMD_CONFIG_DIALOG` - enable config dialog
- `CLOUDCMD_CONFIG_AUTH` - enable auth change in config dialog

View file

@ -1,15 +1,16 @@
#!/usr/bin/env node
import process from 'node:process';
import {createRequire} from 'node:module';
import {promisify} from 'node:util';
import tryToCatch from 'try-to-catch';
import {createSimport} from 'simport';
import parse from 'yargs-parser';
import process from 'node:process';
import exit from '../server/exit.js';
import {createConfig, configPath} from '../server/config.js';
import env from '../server/env.js';
import prefixer from '../server/prefixer.js';
import * as validate from '../server/validate.mjs';
process.on('unhandledRejection', exit);
@ -61,6 +62,7 @@ const yargsOptions = {
'terminal-path',
'terminal-command',
'columns',
'theme',
'import-url',
'import-token',
'export-token',
@ -112,6 +114,7 @@ const yargsOptions = {
'contact': choose(env.bool('contact'), config('contact')),
'terminal': choose(env.bool('terminal'), config('terminal')),
'columns': env('columns') || config('columns') || '',
'theme': env('theme') || config('theme') || '',
'vim': choose(env.bool('vim'), config('vim')),
'log': config('log'),
@ -174,6 +177,9 @@ async function main() {
if (args.repl)
repl();
validate.columns(args.columns);
validate.theme(args.theme);
port(args.port);
config('name', args.name);
@ -194,6 +200,7 @@ async function main() {
config('prefixSocket', prefixer(args.prefixSocket));
config('root', args.root || '/');
config('vim', args.vim);
config('theme', args.theme);
config('columns', args.columns);
config('log', args.log);
config('confirmCopy', args.confirmCopy);
@ -221,6 +228,7 @@ async function main() {
prefix: config('prefix'),
prefixSocket: config('prefixSocket'),
columns: config('columns'),
theme: config('theme'),
};
const password = env('password') || args.password;
@ -228,7 +236,7 @@ async function main() {
if (password)
config('password', await getPassword(password));
await validateRoot(options.root, config);
validateRoot(options.root, config);
if (args.showConfig)
await showConfig();
@ -245,8 +253,7 @@ async function main() {
await importConfig(config);
}
async function validateRoot(root, config) {
const validate = await simport(`${DIR_SERVER}validate.js`);
function validateRoot(root, config) {
validate.root(root, config);
if (root === '/')

View file

@ -1,10 +1,7 @@
'use strict';
const process = require('node:process');
require('../css/main.css');
require('../css/nojs.css');
require('../css/columns/name-size-date.css');
require('../css/columns/name-size.css');
require('./css');
const wraptile = require('wraptile');
const load = require('load.js');

8
client/css.js Normal file
View file

@ -0,0 +1,8 @@
'use strict';
require('../css/main.css');
require('../css/nojs.css');
require('../css/columns/name-size-date.css');
require('../css/columns/name-size.css');
require('../css/themes/light.css');
require('../css/themes/dark.css');

View file

@ -137,6 +137,7 @@ async function fillTemplate() {
editor,
packer,
columns,
theme,
configAuth,
...obj
} = input.convert(config);
@ -144,6 +145,7 @@ async function fillTemplate() {
obj[`${editor}-selected`] = 'selected';
obj[`${packer}-selected`] = 'selected';
obj[`${columns}-selected`] = 'selected';
obj[`${theme}-selected`] = 'selected';
obj.configAuth = configAuth ? '' : 'hidden';
const innerHTML = rendy(Template, obj);

View file

@ -1,5 +1,3 @@
/* @import url(./themes/dark.css); */
@import url(./themes/light.css);
@import url(./urls.css);
@import url(./reset.css);
@import url(./style.css);

View file

@ -41,6 +41,7 @@ code {
-ms-user-select: initial;
-o-user-select: initial;
user-select: text;
color: var(--column-color);
}
.panel,

View file

@ -1,4 +0,0 @@
:root {
--text-color: rgb(49 123 249);
--border-color: rgb(49 123 249 / 40%);
}

View file

@ -15,6 +15,9 @@
<style data-name="columns">
{{ columns }}
</style>
<style data-name="themes">
{{ themes }}
</style>
</head>
<body>
<div class="fm">{{ fm }}</div>

View file

@ -34,6 +34,7 @@
"showFileName": false,
"vim": false,
"columns": "name-size-date-owner-mode",
"theme": "light",
"export": false,
"exportToken": "root",
"import": false,

View file

@ -32,6 +32,7 @@
"--terminal-auto-restart ": "restart command on exit",
"--vim ": "enable vim hot keys",
"--columns ": "set visible columns",
"--theme ": "set theme 'light' or 'dark'",
"--export ": "enable export of config through a server",
"--export-token ": "authorization token used by export server",
"--import ": "enable import of config",

View file

@ -55,6 +55,7 @@ programs in browser from any computer, mobile or tablet device.
--terminal-auto-restart restart command on exit
--vim enable vim hot keys
--columns set visible columns
--theme set theme 'light' or 'dark'
--export enable export of config through a server
--export-token authorization token used by export server
--import enable import of config

View file

@ -21,7 +21,7 @@ import modulas from './modulas.js';
import userMenu from './user-menu.mjs';
import rest from './rest/index.js';
import route from './route.mjs';
import validate from './validate.js';
import * as validate from './validate.mjs';
import prefixer from './prefixer.js';
import terminal from './terminal.js';
import distribute from './distribute/index.js';
@ -64,7 +64,7 @@ function cloudcmd(params) {
if (/root/.test(name))
validate.root(value, config);
if (/editor|packer|columns/.test(name))
if (/editor|packer|themes/.test(name))
validate[name](value);
if (/prefix/.test(name))

View file

@ -1,13 +1,14 @@
'use strict';
const fullstore = require('fullstore');
const process = require('node:process');
const path = require('node:path');
const fs = require('node:fs');
const {nanomemoize} = require('nano-memoize');
const readFilesSync = require('@cloudcmd/read-files-sync');
import path, {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import process from 'node:process';
import fs from 'node:fs';
import fullstore from 'fullstore';
import nanomemoizeDefault from 'nano-memoize';
import readFilesSync from '@cloudcmd/read-files-sync';
const {nanomemoize} = nanomemoizeDefault;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isMap = (a) => /\.map$/.test(a);
const not = (fn) => (a) => !fn(a);
@ -19,9 +20,9 @@ const defaultColumns = {
const _isDev = fullstore(process.env.NODE_ENV === 'development');
const getDist = (isDev) => isDev ? 'dist-dev' : 'dist';
module.exports.isDev = _isDev;
export const isDev = _isDev;
module.exports.getColumns = ({isDev = _isDev()} = {}) => {
export const getColumns = ({isDev = _isDev()} = {}) => {
const columns = readFilesSyncMemo(isDev);
return {

View file

@ -1,8 +1,11 @@
'use strict';
import {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import test from 'supertape';
import fs from 'node:fs';
import {getColumns, isDev} from './columns.mjs';
const test = require('supertape');
const fs = require('node:fs');
const {getColumns, isDev} = require('./columns');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
test('columns: prod', (t) => {
const columns = getColumns({

View file

@ -13,8 +13,9 @@ import {contentType} from 'mime-types';
import root from './root.js';
import prefixer from './prefixer.js';
import CloudFunc from '../common/cloudfunc.js';
import {getColumns} from './columns.js';
import Template from './template.js';
import {getColumns} from './columns.mjs';
import {getThemes} from './theme.mjs';
const require = createRequire(import.meta.url);
const {stringify} = JSON;
@ -164,6 +165,7 @@ function indexProcessing(config, options) {
prefix: getPrefix(config),
config: stringify(config('*')),
columns: getColumns()[config('columns')],
themes: getThemes()[config('theme')],
});
return data;

33
server/theme.mjs Normal file
View file

@ -0,0 +1,33 @@
import path, {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import process from 'node:process';
import fs from 'node:fs';
import fullstore from 'fullstore';
import nanomemoizeDefault from 'nano-memoize';
import readFilesSync from '@cloudcmd/read-files-sync';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isMap = (a) => /\.map$/.test(a);
const not = (fn) => (a) => !fn(a);
const _isDev = fullstore(process.env.NODE_ENV === 'development');
const getDist = (isDev) => isDev ? 'dist-dev' : 'dist';
export const isDev = _isDev;
export const getThemes = ({isDev = _isDev()} = {}) => {
return readFilesSyncMemo(isDev);
};
const {nanomemoize} = nanomemoizeDefault;
const readFilesSyncMemo = nanomemoize((isDev) => {
const dist = getDist(isDev);
const themesDir = path.join(__dirname, '..', dist, 'themes');
const names = fs
.readdirSync(themesDir)
.filter(not(isMap));
return readFilesSync(themesDir, names, 'utf8');
});

31
server/themes.spec.mjs Normal file
View file

@ -0,0 +1,31 @@
import {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import test from 'supertape';
import fs from 'node:fs';
import {getThemes, isDev} from './theme.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
test('themes: dev', (t) => {
const themes = getThemes({
isDev: true,
});
const css = fs.readFileSync(`${__dirname}/../css/themes/dark.css`, 'utf8');
t.equal(themes.dark, css);
t.end();
});
test('themes: no args', (t) => {
const currentIsDev = isDev();
isDev(true);
const themes = getThemes();
const css = fs.readFileSync(`${__dirname}/../css/themes/light.css`, 'utf8');
isDev(currentIsDev);
t.equal(themes.light, css);
t.end();
});

View file

@ -1,13 +1,12 @@
'use strict';
import {statSync as _statSync} from 'node:fs';
import tryCatch from 'try-catch';
import _exit from './exit.js';
import {getColumns as _getColumns} from './columns.mjs';
import {getThemes as _getThemes} from './theme.mjs';
const {statSync: _statSync} = require('node:fs');
const tryCatch = require('try-catch');
const _exit = require('./exit');
const {getColumns: _getColumns} = require('./columns');
const isString = (a) => typeof a === 'string';
module.exports.root = (dir, config, {exit = _exit, statSync = _statSync} = {}) => {
export const root = (dir, config, {exit = _exit, statSync = _statSync} = {}) => {
if (!isString(dir))
throw Error('dir should be a string');
@ -23,21 +22,21 @@ module.exports.root = (dir, config, {exit = _exit, statSync = _statSync} = {}) =
return exit('cloudcmd --root: %s', error.message);
};
module.exports.editor = (name, {exit = _exit} = {}) => {
export const editor = (name, {exit = _exit} = {}) => {
const reg = /^(dword|edward|deepword)$/;
if (!reg.test(name))
exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only');
};
module.exports.packer = (name, {exit = _exit} = {}) => {
export const packer = (name, {exit = _exit} = {}) => {
const reg = /^(tar|zip)$/;
if (!reg.test(name))
exit('cloudcmd --packer: could be "tar" or "zip" only');
};
module.exports.columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => {
export const columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => {
const addQuotes = (a) => `"${a}"`;
const all = Object
.keys(getColumns())
@ -51,3 +50,18 @@ module.exports.columns = (type, {exit = _exit, getColumns = _getColumns} = {}) =
if (!all.includes(type))
exit(`cloudcmd --columns: can be only one of: ${names}`);
};
export const theme = (type, {exit = _exit, getThemes = _getThemes} = {}) => {
const addQuotes = (a) => `"${a}"`;
const all = Object
.keys(getThemes())
.concat('');
const names = all
.filter(Boolean)
.map(addQuotes)
.join(', ');
if (!all.includes(type))
exit(`cloudcmd --theme: can be only one of: ${names}`);
};

View file

@ -1,6 +1,6 @@
import {test, stub} from 'supertape';
import tryCatch from 'try-catch';
import validate from './validate.js';
import * as validate from './validate.mjs';
import cloudcmd from './cloudcmd.mjs';
test('validate: root: bad', (t) => {
@ -102,3 +102,32 @@ test('validate: columns: wrong', (t) => {
t.calledWith(exit, [msg], 'should call exit');
t.end();
});
test('validate: theme', (t) => {
const exit = stub();
validate.theme('dark', {
exit,
});
t.notCalled(exit, 'should not call exit');
t.end();
});
test('validate: theme: wrong', (t) => {
const getThemes = stub().returns({
light: '',
dark: '',
});
const exit = stub();
const msg = 'cloudcmd --theme: can be only one of: "light", "dark"';
validate.theme('hello', {
exit,
getThemes,
});
t.calledWith(exit, [msg], 'should call exit');
t.end();
});

View file

@ -43,8 +43,14 @@
Zip
</label>
</li>
<li title="Theme">
<select data-name="js-theme" class="form-control full-width" title="Theme">
<option {{ light-selected }}>light</option>
<option {{ dark-selected }}>dark</option>
</select>
</li>
<li title="Visible Columns">
<select data-name="js-columns" class="form-control full-width" title="Visible Columns">
<select data-name="js-themes" class="form-control full-width" title="Visible Columns">
<option {{ name-size-date-owner-mode-selected }}>name-size-date-owner-mode</option>
<option {{ name-size-date-selected }}>name-size-date</option>
<option {{ name-size-selected }}>name-size</option>