feat: code cleanup, new features and v2.0

We're merging this to continue https://github.com/filebrowser/filebrowser/pull/575 and setup translations auto-updating.
pull/739/head
Henrique Dias 2019-01-05 16:12:09 +00:00 committed by GitHub
parent 2642333928
commit 39be89780e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
155 changed files with 16499 additions and 6583 deletions

View File

@ -1,13 +0,0 @@
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": ["transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": [ "istanbul" ]
}
}
}

View File

@ -1,2 +0,0 @@
build/*.js
config/*.js

View File

@ -1,27 +0,0 @@
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}

79
.gitignore vendored
View File

@ -1,66 +1,21 @@
# Logs
logs
*.log
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
package-lock.json
yarn.lock
dist/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -1,6 +0,0 @@
.*
static/
src/
.circleci/
build/
/index.html

View File

@ -1,6 +1,8 @@
language: node_js
node_js:
- 10.8.0
script:
- npm run lint
- npm run build

10
.tx/config Normal file
View File

@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw
[file-browser.file-browser]
file_filter = src/i18n/<lang>.json
minimum_perc = 50
source_file = src/i18n/en.json
source_lang = en
type = KEYVALUEJSON

202
LICENSE
View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 File Browser contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

View File

@ -1,29 +0,0 @@
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('./config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
})
})

View File

@ -1,26 +0,0 @@
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '{{ .baseurl }}/',
build: {
env: {
NODE_ENV: '"production"'
},
productionSourceMap: true,
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: {
NODE_ENV: '"development"'
},
produceSourceMap: true
}
}

View File

@ -1,33 +0,0 @@
process.env.NODE_ENV = 'development'
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('./config')
var webpackConfig = require('./webpack.dev.conf')
var fs = require('fs')
if (fs.existsSync('./rice-box.go')) {
fs.unlinkSync('./rice-box.go')
}
if (fs.existsSync('./plugins/rice-box.go')) {
fs.unlinkSync('./plugins/rice-box.go')
}
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
})
})

View File

@ -1,17 +0,0 @@
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// In the production build, this file is replaced with an actual service worker
// file that will precache your site's local assets.
// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' }).then(windowClients => {
for (let windowClient of windowClients) {
// Force open pages to refresh, so that they have a chance to load the
// fresh navigation response from the local dev server.
windowClient.navigate(windowClient.url);
}
});
});

View File

@ -1,55 +0,0 @@
(function() {
'use strict';
// Check to make sure service workers are supported in the current browser,
// and that the current page is accessed from a secure origin. Using a
// service worker from an insecure origin will trigger JS console errors.
var isLocalhost = Boolean(window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
window.addEventListener('load', function() {
if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || isLocalhost)) {
navigator.serviceWorker.register('{{ .baseurl }}/sw.js')
.then(function(registration) {
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {
// updatefound is also fired the very first time the SW is installed,
// and there's no need to prompt for a reload at that point.
// So check here to see if the page is already controlled,
// i.e. whether there's an existing service worker.
if (navigator.serviceWorker.controller) {
// The updatefound event implies that registration.installing is set
var installingWorker = registration.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
// At this point, the old content will have been purged and the
// fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in the page's interface.
break;
case 'redundant':
throw new Error('The installing ' +
'service worker became redundant.');
default:
// Ignore
}
};
}
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
});
})();

View File

@ -1,70 +0,0 @@
var path = require('path')
var config = require('./config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = config.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}

View File

@ -1,12 +0,0 @@
var utils = require('./utils')
var config = require('./config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.produceSourceMap,
extract: isProduction
})
}

View File

@ -1,69 +0,0 @@
var path = require('path')
var utils = require('./utils')
var config = require('./config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.assetsRoot,
filename: '[name].js',
publicPath: config.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.(yml|yaml)$/,
loader: 'yml-loader'
},
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
// limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}

View File

@ -1,81 +0,0 @@
var fs = require('fs')
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('./config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(baseWebpackConfig, {
watch: true,
module: {
rules: utils.styleLoaders({
sourceMap: config.dev.produceSourceMap,
extract: true
})
},
devtool: '#cheap-module-eval-source-map',
output: {
path: config.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.index,
template: './index.html',
inject: true,
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency',
serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname,
'./service-worker-dev.js'), 'utf-8')}</script>`
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.assetsSubDirectory,
ignore: ['.*']
},
{
from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'),
to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js')
}
])
]
})

View File

@ -1,135 +0,0 @@
var fs = require('fs')
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('./config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
var UglifyJS = require('uglify-js')
var env = config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.assetsSubDirectory,
ignore: ['.*']
},
{
from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'),
to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js'),
transform: function (source, path) {
let result = UglifyJS.minify(source.toString('utf8'))
if (result.error !== undefined) {
return source
}
return result.code
}
}
]),
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.index,
template: './index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
minifyCSS: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency',
serviceWorkerLoader: (() => {
let sw = fs.readFileSync(path.join(__dirname, './service-worker-prod.js'), 'utf-8')
let result = UglifyJS.minify(sw)
if (result.error == null) {
sw = result.code
}
return '<script>' + sw + '</script>'
})()
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// service worker caching
new SWPrecacheWebpackPlugin({
cacheId: 'File Browser',
filename: 'sw.js',
replacePrefix: '{{ .baseurl }}/',
staticFileGlobs: ['dist/**/*.{js,html,css}'],
minify: true,
stripPrefix: 'dist/'
})
]
})
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

10983
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,50 @@
{
"name": "filebrowser-frontend",
"version": "1.5.0",
"author": "File Browser contributors",
"contributors": [
"Henrique Dias <hacdias@gmail.com>"
],
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "node ./build/dev.js",
"build": "node ./build/build.js",
"lint": "eslint --ext .js,.vue src"
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"watch": "vue-cli-service build --watch",
"lint": "vue-cli-service lint --fix"
},
"dependencies": {
"clipboard": "^2.0.0",
"codemirror": "^5.36.0",
"filesize": "^3.6.1",
"js-base64": "^2.4.3",
"moment": "^2.22.0",
"normalize.css": "^8.0.0",
"ace-builds": "^1.4.2",
"clipboard": "^2.0.4",
"js-base64": "^2.5.0",
"lodash.clonedeep": "^4.5.0",
"material-design-icons": "^3.0.1",
"moment": "^2.23.0",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"vue": "^2.5.16",
"vue-i18n": "^7.6.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
"vue": "^2.5.21",
"vue-i18n": "^8.7.0",
"vue-router": "^3.0.2",
"vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"autoprefixer": "^8.2.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.4",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"chalk": "^2.3.2",
"connect-history-api-fallback": "^1.5.0",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"http-proxy-middleware": "^0.18.0",
"opn": "^5.3.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^2.0.0",
"rimraf": "^2.6.2",
"semver": "^5.5.0",
"shelljs": "^0.8.1",
"sw-precache-webpack-plugin": "^0.11.5",
"uglify-js": "^3.1.10",
"url-loader": "^1.0.1",
"vue-loader": "^14.2.2",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^2.0.4",
"webpack-hot-middleware": "^2.21.2",
"webpack-merge": "^4.1.2",
"yml-loader": "^2.1.0"
"@vue/cli-plugin-babel": "^3.2.2",
"@vue/cli-plugin-eslint": "^3.2.2",
"@vue/cli-service": "^3.2.3",
"babel-eslint": "^10.0.1",
"eslint": "^5.12.0",
"eslint-plugin-vue": "^5.1.0",
"vue-template-compiler": "^2.5.21"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 843 B

After

Width:  |  Height:  |  Size: 843 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -5,42 +5,30 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Inject some variables -->
<meta name="base" content="{{ .baseurl }}">
<meta name="staticgen" content="{{ .StaticGen }}">
<meta name="noauth" content="{{ .NoAuth }}">
<meta name="version" content="{{ .Version }}">
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
[{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}]
<title>File Browser</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .baseurl }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .baseurl }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .baseurl }}/static/img/icons/favicon.ico"><![endif]-->
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<link rel="icon" type="image/png" sizes="32x32" href="/[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" href="{{ .baseurl }}/static/manifest.json">
<link rel="manifest" href="/[{[ .StaticURL ]}]/manifest.json">
<meta name="theme-color" content="#2979ff">
<!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .baseurl }}/static/img/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .baseurl }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileImage" content="/[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<script>CSS = "{{ .CSS }}"</script>
{{ if .ReCaptcha -}}
<script src="{{ .ReCaptchaHost }}/recaptcha/api.js?render=explicit"></script>
{{ end }}
<% for (var chunk of webpack.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .baseurl }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Inject Some Variables -->
<script>window.FileBrowser = JSON.parse(`[{[ .Json ]}]`)</script>
<style>
#loading {
@ -116,6 +104,8 @@
</div>
</div>
<%= htmlWebpackPlugin.options.serviceWorkerLoader %>
[{[ if .CSS -]}]
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</body>
</html>

View File

@ -3,17 +3,17 @@
"short_name": "File Browser",
"icons": [
{
"src": "{{ .baseurl }}/static/img/icons/android-chrome-192x192.png",
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "{{ .baseurl }}/static/img/icons/android-chrome-512x512.png",
"src": "./static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "{{ .baseurl }}/",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#455a64"

View File

@ -1,74 +1,19 @@
<template>
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
<div>
<router-view></router-view>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'app',
computed: mapState(['recaptcha']),
data () {
return {
loaded: false
}
},
mounted () {
if (this.recaptcha.length === 0) {
this.unload()
return
}
const loading = document.getElementById('loading')
loading.classList.add('done')
let check = () => {
if (typeof window.grecaptcha === 'undefined') {
setTimeout(check, 100)
return
}
this.unload()
}
check()
},
methods: {
unload () {
this.loaded = true
// Remove loading animation.
let loading = document.getElementById('loading')
loading.classList.add('done')
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
this.updateCSS()
},
updateCSS (global = false) {
let css = this.$store.state.css
if (typeof this.$store.state.user.css === 'string' && !global) {
css += '\n' + this.$store.state.user.css
}
this.removeCSS()
let style = document.createElement('style')
style.title = 'custom-css'
style.type = 'text/css'
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
},
removeCSS () {
let style = document.querySelector('style[title="custom-css"]')
if (style === undefined || style === null) {
return
}
style.parentElement.removeChild(style)
},
cleanCSS () {
this.updateCSS(true)
}
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
}
}
</script>

16
src/api/commands.js Normal file
View File

@ -0,0 +1,16 @@
import { removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
const ssl = (window.location.protocol === 'https:')
const protocol = (ssl ? 'wss:' : 'ws:')
export default function command(url, command, onmessage, onclose) {
url = removePrefix(url)
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command)
conn.onmessage = onmessage
conn.onclose = onclose
}

139
src/api/files.js Normal file
View File

@ -0,0 +1,139 @@
import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
export async function fetch (url) {
url = removePrefix(url)
const res = await fetchURL(`/api/resources${url}`, {})
if (res.status === 200) {
let data = await res.json()
data.url = `/files${data.path}`
if (data.isDir) {
if (!data.url.endsWith('/')) data.url += '/'
data.items = data.items.map((item, index) => {
item.index = index
item.url = `${data.url}${encodeURIComponent(item.name)}`
if (item.isDir) {
item.url += '/'
}
return item
})
}
return data
} else {
throw new Error(res.status)
}
}
async function resourceAction (url, method, content) {
url = removePrefix(url)
let opts = { method }
if (content) {
opts.body = content
}
const res = await fetchURL(`/api/resources${url}`, opts)
if (res.status !== 200) {
throw new Error(res.responseText)
} else {
return res
}
}
export async function remove (url) {
return resourceAction(url, 'DELETE')
}
export async function put (url, content = '') {
return resourceAction(url, 'PUT', content)
}
export function download (format, ...files) {
let url = `${baseURL}/api/raw`
if (files.length === 1) {
url += removePrefix(files[0]) + '?'
} else {
let arg = ''
for (let file of files) {
arg += removePrefix(file) + ','
}
arg = arg.substring(0, arg.length - 1)
arg = encodeURIComponent(arg)
url += `/?files=${arg}&`
}
if (format !== null) {
url += `algo=${format}&`
}
url += `auth=${store.state.jwt}`
window.open(url)
}
export async function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (typeof onupload === 'function') {
request.upload.onprogress = onupload
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else if (request.status === 409) {
reject(request.status)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => {
reject(error)
}
request.send(content)
})
}
function moveCopy (items, copy = false) {
let promises = []
for (let item of items) {
let from = removePrefix(item.from)
let to = encodeURIComponent(removePrefix(item.to))
let url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
promises.push(resourceAction(url, 'PATCH'))
}
return Promise.all(promises)
}
export function move (items) {
return moveCopy(items)
}
export function copy (items) {
return moveCopy(items, true)
}
export async function checksum (url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
return (await data.json()).checksums[algo]
}

15
src/api/index.js Normal file
View File

@ -0,0 +1,15 @@
import * as files from './files'
import * as share from './share'
import * as users from './users'
import * as settings from './settings'
import search from './search'
import commands from './commands'
export {
files,
share,
users,
settings,
commands,
search
}

8
src/api/search.js Normal file
View File

@ -0,0 +1,8 @@
import { fetchJSON, removePrefix } from './utils'
export default async function search (url, query) {
url = removePrefix(url)
query = encodeURIComponent(query)
return fetchJSON(`/api/search${url}?query=${query}`, {})
}

16
src/api/settings.js Normal file
View File

@ -0,0 +1,16 @@
import { fetchURL, fetchJSON } from './utils'
export function get () {
return fetchJSON(`/api/settings`, {})
}
export async function update (settings) {
const res = await fetchURL(`/api/settings`, {
method: 'PUT',
body: JSON.stringify(settings)
})
if (res.status !== 200) {
throw new Error(res.status)
}
}

32
src/api/share.js Normal file
View File

@ -0,0 +1,32 @@
import { fetchURL, fetchJSON, removePrefix } from './utils'
export async function getHash(hash) {
return fetchJSON(`/api/public/share/${hash}`)
}
export async function get(url) {
url = removePrefix(url)
return fetchJSON(`/api/share${url}`)
}
export async function remove(hash) {
const res = await fetchURL(`/api/share/${hash}`, {
method: 'DELETE'
})
if (res.status !== 200) {
throw new Error(res.status)
}
}
export async function create(url, expires = '', unit = 'hours') {
url = removePrefix(url)
url = `/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
}
return fetchJSON(url, {
method: 'POST'
})
}

52
src/api/users.js Normal file
View File

@ -0,0 +1,52 @@
import { fetchURL, fetchJSON } from './utils'
export async function getAll () {
return fetchJSON(`/api/users`, {})
}
export async function get (id) {
return fetchJSON(`/api/users/${id}`, {})
}
export async function create (user) {
const res = await fetchURL(`/api/users`, {
method: 'POST',
body: JSON.stringify({
what: 'user',
which: [],
data: user
})
})
if (res.status === 201) {
return res.headers.get('Location')
} else {
throw new Error(res.status)
}
}
export async function update (user, which = ['all']) {
const res = await fetchURL(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify({
what: 'user',
which: which,
data: user
})
})
if (res.status !== 200) {
throw new Error(res.status)
}
}
export async function remove (id) {
const res = await fetchURL(`/api/users/${id}`, {
method: 'DELETE'
})
if (res.status !== 200) {
throw new Error(res.status)
}
}

45
src/api/utils.js Normal file
View File

@ -0,0 +1,45 @@
import store from '@/store'
import { renew } from '@/utils/auth'
import { baseURL } from '@/utils/constants'
export async function fetchURL (url, opts) {
opts = opts || {}
opts.headers = opts.headers || {}
let { headers, ...rest } = opts
const res = await fetch(`${baseURL}${url}`, {
headers: {
'Authorization': `Bearer ${store.state.jwt}`,
...headers
},
...rest
})
if (res.headers.get('X-Renew-Token') === 'true') {
await renew(store.state.jwt)
}
return res
}
export async function fetchJSON (url, opts) {
const res = await fetchURL(url, opts)
if (res.status === 200) {
return res.json()
} else {
throw new Error(res.status)
}
}
export function removePrefix (url) {
if (url.startsWith('/files')) {
url = url.slice(6)
}
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
return url
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,62 +4,56 @@
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
<i class="material-icons">menu</i>
</button>
<img src="../assets/logo.svg" alt="File Browser">
<search></search>
<img :src="logoURL" alt="File Browser">
<search v-if="isLogged"></search>
</div>
<div>
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
<i class="material-icons">search</i>
</button>
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
<i class="material-icons">save</i>
</button>
<template v-if="staticGen.length > 0">
<button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
<i class="material-icons">send</i>
<template v-if="isLogged">
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
<i class="material-icons">search</i>
</button>
</template>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
<i class="material-icons">save</i>
</button>
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showRenameButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showRenameButton"></share-button>
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && isListing">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<template v-if="staticGen.length > 0">
<schedule-button v-show="showPublishButton"></schedule-button>
</template>
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<switch-button v-show="showSwitchButton"></switch-button>
<download-button v-show="showCommonButton"></download-button>
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="showCommonButton"></info-button>
<shell-button v-show="user.perm.execute" />
<switch-button v-show="isListing"></switch-button>
<download-button v-show="showDownloadButton"></download-button>
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="isFiles"></info-button>
<button v-show="isListing" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
<i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span>
</button>
</div>
</template>
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
<i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span>
</button>
</div>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</header>
@ -75,14 +69,15 @@ import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import ScheduleButton from './buttons/Schedule'
import ShareButton from './buttons/Share'
import ShellButton from './buttons/Shell'
import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api'
import { logoURL } from '@/utils/constants'
import * as api from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'main',
name: 'header-layout',
components: {
Search,
InfoButton,
@ -94,7 +89,7 @@ export default {
UploadButton,
SwitchButton,
MoveButton,
ScheduleButton
ShellButton
},
data: function () {
return {
@ -114,88 +109,62 @@ export default {
},
computed: {
...mapGetters([
'selectedCount'
'selectedCount',
'isFiles',
'isEditor',
'isListing',
'isLogged'
]),
...mapState([
'req',
'user',
'loading',
'reload',
'multiple',
'staticGen'
'multiple'
]),
logoURL: () => logoURL,
isMobile () {
return this.width <= 736
},
isListing () {
return this.req.kind === 'listing'
},
showSelectButton () {
return this.req.kind === 'listing' && !this.loading && this.$route.name === 'Files'
showUpload () {
return this.isListing && this.user.perm.create
},
showSaveButton () {
return (this.req.kind === 'editor' && !this.loading)
return this.isEditor && this.user.perm.modify
},
showPublishButton () {
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
},
showSwitchButton () {
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
},
showCommonButton () {
return !(this.$route.name !== 'Files' || this.loading)
},
showUpload () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'editor') return false
return this.user.allowNew
showDownloadButton () {
return this.isFiles && this.user.perm.download
},
showDeleteButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'listing') {
if (this.selectedCount === 0) {
return false
}
return this.user.allowEdit
}
return this.user.allowEdit
return this.isFiles && (this.isListing
? (this.selectedCount !== 0 && this.user.perm.delete)
: this.user.perm.delete)
},
showRenameButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'listing') {
if (this.selectedCount === 1) {
return this.user.allowEdit
}
return false
}
return this.user.allowEdit
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.rename)
: this.user.perm.rename)
},
showShareButton () {
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.share)
: this.user.perm.share)
},
showMoveButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind !== 'listing') {
return false
}
if (this.selectedCount > 0) {
return this.user.allowEdit
}
return false
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.rename)
: this.user.perm.rename)
},
showCopyButton () {
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.create)
: this.user.perm.create)
},
showMore () {
if (this.$route.name !== 'Files' || this.loading) return false
return (this.$store.state.show === 'more')
return this.isFiles && this.$store.state.show === 'more'
},
showOverlay () {
return (this.$store.state.show === 'more')
return this.showMore
}
},
methods: {

View File

@ -1,44 +1,53 @@
<template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="input">
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
<button
v-if="active"
class="action"
@click="close"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')"
>
<i class="material-icons">arrow_back</i>
</button>
<i v-else class="material-icons">search</i>
<input type="text"
@keyup="keyup"
<input
type="text"
@keyup.exact="keyup"
@keyup.enter="submit"
ref="input"
:autofocus="active"
v-model.trim="value"
:aria-label="$t('search.writeToSearch')"
:placeholder="placeholder">
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
>
</div>
<div id="result">
<div>
<template v-if="search.length === 0 && commands.length === 0">
<template v-if="isEmpty">
<p>{{ text }}</p>
<template v-if="value.length === 0">
<div class="boxes">
<h3>{{ $t('search.types') }}</h3>
<div>
<div tabindex="0"
<div
tabindex="0"
v-for="(v,k) in boxes"
:key="k"
role="button"
@click="init('type:'+k)"
:aria-label="$t('search.'+v.label)">
:aria-label="$t('search.'+v.label)"
>
<i class="material-icons">{{v.icon}}</i>
<p>{{ $t('search.'+v.label) }}</p>
</div>
</div>
</div>
</template>
</template>
<ul v-else-if="search.length > 0">
<ul v-else-if="results.length > 0">
<li v-for="(s,k) in results" :key="k">
<router-link @click.native="close" :to="'./' + s.path">
<i v-if="s.dir" class="material-icons">folder</i>
@ -47,228 +56,129 @@
</router-link>
</li>
</ul>
<pre v-else-if="commands.length > 0"><template v-for="c in commands">{{ c }}</template></pre>
</div>
<p id="renew"><i class="material-icons spin">autorenew</i></p>
<p id="renew">
<i class="material-icons spin">autorenew</i>
</p>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import * as api from '@/utils/api'
import { mapState, mapGetters, mapMutations } from "vuex"
import url from "@/utils/url"
import { search } from "@/api"
// TODO: show fifty at the tie
var boxes = {
image: { label: 'images', icon: 'insert_photo' },
audio: { label: 'music', icon: 'volume_up' },
video: { label: 'video', icon: 'movie' },
pdf: { label: 'pdf', icon: 'picture_as_pdf' }
image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }
}
export default {
name: 'search',
data: function () {
name: "search",
data: function() {
return {
value: '',
value: "",
active: false,
ongoing: false,
scrollable: null,
search: [],
commands: [],
results: [],
reload: false,
resultsCount: 50,
boxes: boxes
scrollable: null
}
},
watch: {
show (val, old) {
this.active = (val === 'search')
show(val, old) {
this.active = val === "search"
// If the hover was search and now it's something else
// we should blur the input.
if (old === 'search' && val !== 'search') {
if (old === "search" && !this.active) {
if (this.reload) {
this.$store.commit('setReload', true)
this.setReload(true)
}
document.body.style.overflow = 'auto'
document.body.style.overflow = "auto"
this.reset()
this.$refs.input.blur()
}
// If we are starting to show the search box, we should
// focus the input.
if (val === 'search') {
} else if (this.active) {
this.reload = false
this.$refs.input.focus()
document.body.style.overflow = 'hidden'
document.body.style.overflow = "hidden"
}
}
},
computed: {
...mapState(['user', 'show']),
// Placeholder value.
placeholder () {
if (this.user.allowCommands && this.user.commands.length > 0) {
return this.$t('search.searchOrCommand')
}
return this.$t('search.search')
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
boxes() {
return boxes
},
// The text that is shown on the results' box while
// there is no search result or command output to show.
text () {
isEmpty() {
return this.results.length === 0
},
text() {
if (this.ongoing) {
return ''
return ""
}
if (this.value.length === 0) {
if (this.user.allowCommands && this.user.commands.length > 0) {
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
}
this.$t('search.typeSearch')
}
if (!(this.value[0] === '$') || !this.user.allowCommands) {
return this.$t('search.pressToSearch')
} else {
if (this.command.length === 0) {
return this.$t('search.typeCommand')
}
if (!this.supported()) {
return this.$t('search.notSupportedCommand')
}
return this.$t('search.pressToExecute')
}
},
// The command, without the leading symbol ('$') with or without a following space (' ')
command () {
return this.value[1] === ' ' ? this.value.slice(2) : this.value.slice(1)
},
results () {
return this.search.slice(0, this.resultsCount)
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
}
},
mounted () {
// Gets the result div which will be scrollable.
this.scrollable = document.querySelector('#search #result')
// Adds the keydown event on window for the ESC key, so
// when it's pressed, it closes the search window.
window.addEventListener('keydown', (event) => {
mounted() {
window.addEventListener("keydown", event => {
if (event.keyCode === 27) {
this.$store.commit('closeHovers')
}
})
this.scrollable.addEventListener('scroll', (event) => {
if (this.scrollable.scrollTop === (this.scrollable.scrollHeight - this.scrollable.offsetHeight)) {
this.resultsCount += 50
this.closeHovers()
}
})
},
methods: {
// Sets the search to active.
open (event) {
this.$store.commit('showHover', 'search')
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search")
},
// Closes the search and prevents the event
// of propagating so it doesn't trigger the
// click event on #search.
close (event) {
close(event) {
event.stopPropagation()
event.preventDefault()
this.$store.commit('closeHovers')
this.closeHovers()
},
// Checks if the current input is a supported command.
supported () {
let cmd = this.command.split(' ')[0]
let cl = this.user.commands.length
if (cl !== 0) {
for (let i = 0; i < cl; i++) {
if (cmd.match(this.user.commands[i])) {
return true
}
}
}
return false
},
// Initializes the search with a default value.
init (string) {
this.value = string + ' '
this.$refs.input.focus()
},
// Resets the search box value.
reset () {
this.value = ''
this.active = false
this.ongoing = false
this.resultsCount = 50
this.search = []
this.commands = []
},
// When the user presses a key, if it is ESC
// then it will close the search box. Otherwise,
// it will set the search box to active and clean
// the search results, as well as commands'.
keyup (event) {
keyup(event) {
if (event.keyCode === 27) {
this.close(event)
return
}
this.search.length = 0
this.commands.length = 0
this.results.length = 0
},
// Submits the input to the server and sets ongoing to true.
submit (event) {
this.ongoing = true
init (string) {
this.value = `${string} `
this.$refs.input.focus()
},
reset () {
this.value = ''
this.active = false
this.ongoing = false
this.resultsCount = 50
this.results = []
},
async submit(event) {
event.preventDefault()
let path = this.$route.path
if (this.$store.state.req.kind !== 'listing') {
path = url.removeLastDir(path) + '/'
}
// In case of being a command.
if (this.value[0] === '$') {
if (this.supported() && this.user.allowCommands) {
api.command(path, this.command,
(event) => {
this.commands.push(event.data)
this.scrollable.scrollTop = this.scrollable.scrollHeight
},
(event) => {
this.reload = true
this.ongoing = false
this.scrollable.scrollTop = this.scrollable.scrollHeight
}
)
return
}
this.ongoing = false
if (this.value === '') {
return
}
let results = []
let path = this.$route.path
if (!this.isListing) {
path = url.removeLastDir(path) + "/"
}
// In case of being a search.
api.search(path, this.value,
(event) => {
let response = JSON.parse(event.data)
if (response.path[0] === '/') {
response.path = response.path.substring(1)
}
this.ongoing = true
results.push(response)
},
(event) => {
this.ongoing = false
this.search = results
}
)
this.results = await search(path, this.value)
this.ongoing = false
}
}
}

115
src/components/Shell.vue Normal file
View File

@ -0,0 +1,115 @@
<template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
<div v-for="(c, index) in content" :key="index" class="shell__result" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" />
</div>
</div>
</template>
<script>
import { mapMutations, mapState, mapGetters } from 'vuex'
import { commands } from '@/api'
export default {
name: 'shell',
computed: {
...mapState([ 'user', 'showShell' ]),
...mapGetters([ 'isFiles', 'isLogged' ]),
path: function () {
if (this.isFiles) {
return this.$route.path
}
return ''
}
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true
}),
methods: {
...mapMutations([ 'toggleShell' ]),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
},
focus: function () {
this.$refs.input.focus()
},
historyUp () {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos]
this.focus()
}
},
historyDown () {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos]
this.focus()
} else {
this.historyPos = this.history.length
this.$refs.input.innerText = ''
}
},
submit: function (event) {
const cmd = event.target.innerText.trim()
if (cmd === '') {
return
}
if (cmd === 'clear') {
this.content = []
event.target.innerHTML = ''
return
}
if (cmd === 'exit') {
event.target.innerHTML = ''
this.toggleShell()
return
}
this.canInput = false
event.target.innerHTML = ''
let results = {
text: `${cmd}\n\n`
}
this.history.push(cmd)
this.historyPos = this.history.length
this.content.push(results)
commands(
this.path,
cmd,
event => {
results.text += `${event.data}\n`
this.scroll()
},
() => {
results.text = results.text.trimEnd()
this.canInput = true
this.$refs.input.focus()
this.scroll()
}
)
}
}
}
</script>

View File

@ -1,89 +1,80 @@
<template>
<nav :class="{active}">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
<i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span>
</router-link>
<div v-if="user.allowNew">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
<i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span>
</button>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
<i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span>
</button>
</div>
<div v-if="staticGen.length > 0">
<router-link to="/files/settings"
:aria-label="$t('sidebar.siteSettings')"
:title="$t('sidebar.siteSettings')"
class="action">
<i class="material-icons">settings</i>
<span>{{ $t('sidebar.siteSettings') }}</span>
<template v-if="isLogged">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
<i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span>
</router-link>
<template v-if="staticGen === 'hugo'">
<button class="action"
:aria-label="$t('sidebar.hugoNew')"
:title="$t('sidebar.hugoNew')"
v-if="user.allowNew"
@click="$store.commit('showHover', 'new-archetype')">
<i class="material-icons">merge_type</i>
<span>{{ $t('sidebar.hugoNew') }}</span>
<div v-if="user.perm.create">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
<i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span>
</button>
</template>
<button class="action"
:aria-label="$t('sidebar.preview')"
:title="$t('sidebar.preview')"
@click="preview">
<i class="material-icons">remove_red_eye</i>
<span>{{ $t('sidebar.preview') }}</span>
</button>
</div>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
<i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span>
</button>
</div>
<div v-if="!$store.state.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
<div>
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
</router-link>
<button v-if="!noAuth" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span>
</button>
</div>
</template>
<template v-else>
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.login') }}</span>
</router-link>
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span>
</button>
</div>
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')">
<i class="material-icons">person_add</i>
<span>{{ $t('sidebar.signup') }}</span>
</router-link>
</template>
<p class="credits">
<span><a rel="noopener noreferrer" href="https://github.com/filebrowser/filebrowser">File Browser</a> v{{ version }}</span>
<span>
<span v-if="disableExternal">File Browser</span>
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a>
<span> v{{ version }}</span>
</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
</template>
<script>
import {mapState} from 'vuex'
import auth from '@/utils/auth'
import { mapState, mapGetters } from 'vuex'
import * as auth from '@/utils/auth'
import { version, signup, disableExternal, noAuth } from '@/utils/constants'
export default {
name: 'sidebar',
computed: {
...mapState(['user', 'staticGen', 'version']),
...mapState([ 'user' ]),
...mapGetters([ 'isLogged' ]),
active () {
return this.$store.state.show === 'sidebar'
}
},
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
noAuth: () => noAuth
},
methods: {
help () {
this.$store.commit('showHover', 'help')
},
preview () {
window.open(this.$store.state.baseURL + '/preview/')
},
logout: auth.logout
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'copy-button',
methods: {
show: function (event) {
show: function () {
this.$store.commit('showHover', 'copy')
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'delete-button',
methods: {
show: function (event) {
show: function () {
this.$store.commit('showHover', 'delete')
}
}

View File

@ -8,30 +8,26 @@
<script>
import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api'
import { files as api } from '@/api'
export default {
name: 'download-button',
computed: {
...mapState(['req', 'selected']),
...mapGetters(['selectedCount'])
...mapGetters(['isListing', 'selectedCount'])
},
methods: {
download: function (event) {
// If we are not on a listing, download the current file.
if (this.req.kind !== 'listing') {
download: function () {
if (!this.isListing) {
api.download(null, this.$route.path)
return
}
// If we are on a listing and there is one element selected,
// download it.
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url)
return
}
// Otherwise show the prompt to choose the formt of the download.
this.$store.commit('showHover', 'download')
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'info-button',
methods: {
show: function (event) {
show: function () {
this.$store.commit('showHover', 'info')
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'move-button',
methods: {
show: function (event) {
show: function () {
this.$store.commit('showHover', 'move')
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'rename-button',
methods: {
show: function (event) {
show: function () {
this.$store.commit('showHover', 'rename')
}
}

View File

@ -1,21 +0,0 @@
<template>
<button @click="show"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')"
id="schedule-button"
class="action">
<i class="material-icons">alarm</i>
<span>{{ $t('buttons.schedule') }}</span>
</button>
</template>
<script>
export default {
name: 'schedule-button',
methods: {
show: function (event) {
this.$store.commit('showHover', 'schedule')
}
}
}
</script>

View File

@ -9,7 +9,7 @@
export default {
name: 'share-button',
methods: {
show (event) {
show () {
this.$store.commit('showHover', 'share')
}
}

View File

@ -0,0 +1,17 @@
<template>
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
<i class="material-icons">code</i>
<span>{{ $t('buttons.shell') }}</span>
</button>
</template>
<script>
export default {
name: 'shell-button',
methods: {
show: function () {
this.$store.commit('toggleShell')
}
}
}
</script>

View File

@ -7,7 +7,7 @@
<script>
import { mapState, mapMutations } from 'vuex'
import { updateUser } from '@/utils/api'
import { users as api } from '@/api'
export default {
name: 'switch-button',
@ -19,17 +19,21 @@ export default {
}
},
methods: {
...mapMutations(['updateUser']),
change: function (event) {
// If we are on mobile we should close the dropdown.
this.$store.commit('closeHovers')
...mapMutations([ 'updateUser', 'closeHovers' ]),
change: async function () {
this.closeHovers()
let user = {...this.user}
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
const data = {
id: this.user.id,
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
}
updateUser(user, 'partial').then(() => {
this.updateUser({ viewMode: user.viewMode })
}).catch(this.$showError)
try {
await api.update(data, ['viewMode'])
this.updateUser(data)
} catch (e) {
this.$showError(e)
}
}
}
}

View File

@ -9,7 +9,7 @@
export default {
name: 'upload-button',
methods: {
upload: function (event) {
upload: function () {
document.getElementById('upload-input').click()
}
}

View File

@ -1,90 +1,51 @@
<template>
<form id="editor" :class="req.language">
<div v-if="hasMetadata" id="metadata">
<h2>{{ $t('files.metadata') }}</h2>
</div>
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
</form>
<form id="editor"></form>
</template>
<script>
import { mapState } from 'vuex'
import CodeMirror from '@/utils/codemirror'
import * as api from '@/utils/api'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
export default {
name: 'editor',
computed: {
...mapState(['req', 'schedule']),
hasMetadata: function () {
return (this.req.metadata !== undefined && this.req.metadata !== null)
}
...mapState(['req'])
},
data: function () {
return {
metadata: null,
metalang: null,
content: null
content: null,
editor: null
}
},
created () {
window.addEventListener('keydown', this.keyEvent)
document.getElementById('save-button').addEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.addEventListener('click', this.publish)
}
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.removeEventListener('click', this.publish)
}
},
mounted: function () {
if (this.req.content === undefined || this.req.content === null) {
this.req.content = ''
}
// Set up the main content editor.
this.content = CodeMirror(document.getElementById('editor'), {
this.editor = ace.edit('editor', {
maxLines: Infinity,
minLines: 20,
value: this.req.content,
lineNumbers: (this.req.language !== 'markdown'),
viewportMargin: 500,
autofocus: true,
mode: this.req.language,
theme: (this.req.language === 'markdown') ? 'markdown' : 'ttcn',
lineWrapping: (this.req.language === 'markdown')
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode
})
CodeMirror.autoLoadMode(this.content, this.req.language)
// Prevent of going on if there is no metadata.
if (!this.hasMetadata) {
return
}
this.parseMetadata()
// Set up metadata editor.
this.metadata = CodeMirror(document.getElementById('metadata'), {
value: this.req.metadata,
viewportMargin: Infinity,
lineWrapping: true,
theme: 'markdown',
mode: this.metalang
})
CodeMirror.autoLoadMode(this.metadata, this.metalang)
},
methods: {
// Saves the content when the user presses CTRL-S.
keyEvent (event) {
if (!event.ctrlKey && !event.metaKey) {
return
@ -97,46 +58,17 @@ export default {
event.preventDefault()
this.save()
},
// Parses the metadata and gets the language in which
// it is written.
parseMetadata () {
if (this.req.metadata.startsWith('{')) {
this.metalang = 'json'
}
async save () {
const button = 'save'
buttons.loading('save')
if (this.req.metadata.startsWith('---')) {
this.metalang = 'yaml'
try {
await api.put(this.$route.path, this.editor.getValue())
buttons.success(button)
} catch (e) {
buttons.done(button)
this.$showError(e)
}
if (this.req.metadata.startsWith('+++')) {
this.metalang = 'toml'
}
},
// Publishes the file.
publish (event) {
this.save(event, true)
},
// Saves the file.
save (event, regenerate = false) {
let button = regenerate ? 'publish' : 'save'
if (this.schedule !== '') button = 'schedule'
let content = this.content.getValue()
buttons.loading(button)
if (this.hasMetadata) {
content = this.metadata.getValue() + '\n\n' + content
}
api.put(this.$route.path, content, regenerate, this.schedule)
.then(() => {
buttons.success(button)
this.$store.commit('setSchedule', '')
})
.catch(error => {
buttons.done(button)
this.$showError(error)
this.$store.commit('setSchedule', '')
})
}
}
}

View File

@ -49,7 +49,6 @@
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0">
<item v-for="(item) in dirs"
v-if="item.isDir"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
@ -64,7 +63,6 @@
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0">
<item v-for="(item) in files"
v-if="!item.isDir"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
@ -88,10 +86,10 @@
</template>
<script>
import {mapState} from 'vuex'
import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import * as api from '@/utils/api'
import { users, files as api } from '@/api'
import buttons from '@/utils/buttons'
export default {
@ -105,24 +103,22 @@ export default {
computed: {
...mapState(['req', 'selected', 'user']),
nameSorted () {
return (this.req.sort === 'name')
return (this.req.sorting.by === 'name')
},
sizeSorted () {
return (this.req.sort === 'size')
return (this.req.sorting.by === 'size')
},
modifiedSorted () {
return (this.req.sort === 'modified')
return (this.req.sorting.by === 'modified')
},
ascOrdered () {
return (this.req.order === 'asc')
return this.req.sorting.asc
},
items () {
const dirs = []
const files = []
this.req.items.forEach((item, index) => {
item.index = index
this.req.items.forEach((item) => {
if (item.isDir) {
dirs.push(item)
} else {
@ -184,6 +180,7 @@ export default {
document.removeEventListener('drop', this.drop)
},
methods: {
...mapMutations([ 'updateUser' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
@ -213,7 +210,10 @@ export default {
event.preventDefault()
},
copyCut (event, key) {
event.preventDefault()
if (event.target.tagName.toLowerCase() === 'input') {
return
}
let items = []
for (let i of this.selected) {
@ -223,6 +223,10 @@ export default {
})
}
if (items.length == 0) {
return
}
this.$store.commit('updateClipboard', {
key: key,
items: items
@ -233,15 +237,21 @@ export default {
return
}
event.preventDefault()
let items = []
for (let item of this.$store.state.clipboard.items) {
items.push({
from: item.from,
to: this.$route.path + item.name
})
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name
if (from === to) {
return
}
items.push({ from, to })
}
if (items.length === 0) {
return
}
if (this.$store.state.clipboard.key === 'x') {
@ -267,7 +277,7 @@ export default {
this.show += 50
}
},
dragEnter (event) {
dragEnter () {
// When the user starts dragging an item, put every
// file on the listing with 50% opacity.
let items = document.getElementsByClassName('item')
@ -276,7 +286,7 @@ export default {
file.style.opacity = 0.5
})
},
dragEnd (event) {
dragEnd () {
this.resetOpacity()
},
drop: function (event) {
@ -391,27 +401,29 @@ export default {
return false
},
sort (sort) {
let order = 'desc'
async sort (by) {
let asc = false
if (sort === 'name') {
if (by === 'name') {
if (this.nameIcon === 'arrow_upward') {
order = 'asc'
asc = true
}
} else if (sort === 'size') {
} else if (by === 'size') {
if (this.sizeIcon === 'arrow_upward') {
order = 'asc'
asc = true
}
} else if (sort === 'modified') {
} else if (by === 'modified') {
if (this.modifiedIcon === 'arrow_upward') {
order = 'asc'
asc = true
}
}
let path = this.$store.state.baseURL
if (path === '') path = '/'
document.cookie = `sort=${sort}; max-age=31536000; path=${path}`
document.cookie = `order=${order}; max-age=31536000; path=${path}`
try {
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
} catch (e) {
this.$showError(e)
}
this.$store.commit('setReload', true)
}
}

View File

@ -33,7 +33,7 @@
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import * as api from '@/utils/api'
import { files as api } from '@/api'
export default {
name: 'item',
@ -65,7 +65,7 @@ export default {
humanTime: function () {
return moment(this.modified).fromNow()
},
dragStart: function (event) {
dragStart: function () {
if (this.selectedCount === 0) {
this.addSelected(this.index)
return
@ -140,7 +140,7 @@ export default {
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
touchstart (event) {
touchstart () {
setTimeout(() => {
this.touches = 0
}, 300)
@ -150,7 +150,7 @@ export default {
this.open()
}
},
open: function (event) {
open: function () {
this.$router.push({path: this.url})
}
}

View File

@ -5,9 +5,9 @@
<i class="material-icons">close</i>
</button>
<rename-button v-if="allowEdit()"></rename-button>
<delete-button v-if="allowEdit()"></delete-button>
<download-button></download-button>
<rename-button v-if="user.perm.rename"></rename-button>
<delete-button v-if="user.perm.delete"></delete-button>
<download-button v-if="user.perm.download"></download-button>
<info-button></info-button>
</div>
@ -19,19 +19,23 @@
</button>
<div class="preview">
<img v-if="req.type == 'image'" :src="raw()">
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
<track v-for="(sub, index) in subtitles" :kind="sub.kind" :src="'/api/subtitle/' + sub.src" :label="sub.label" :default="index === 0">
<img v-if="req.type == 'image'" :src="raw">
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
<track
kind="captions"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index" :default="index === 0">
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download()">download it</a>
but don't worry, you can <a :href="download">download it</a>
and watch it with your favorite video player!
</video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
<a v-else-if="req.type == 'blob'" :href="download()">
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
<pre v-else >{{ req.content }}</pre>
</div>
</div>
</template>
@ -39,12 +43,20 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import * as api from '@/utils/api'
import { baseURL } from '@/utils/constants'
import { files as api } from '@/api'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
const mediaTypes = [
"image",
"video",
"audio",
"blob"
]
export default {
name: 'preview',
components: {
@ -62,44 +74,44 @@ export default {
}
},
computed: {
...mapState(['req', 'oldReq']),
...mapState(['req', 'user', 'oldReq', 'jwt']),
hasPrevious () {
return (this.previousLink !== '')
},
hasNext () {
return (this.nextLink !== '')
},
download () {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
raw () {
return `${this.download}&inline=true`
}
},
mounted () {
async mounted () {
window.addEventListener('keyup', this.key)
api.fetch(url.removeLastDir(this.$route.path))
.then(req => {
this.listing = req
this.updateLinks()
})
.catch(this.$showError)
if (this.req.type === 'audio' || this.req.type === 'video') {
api.subtitles(this.req.url.slice(6))
.then(req => {
this.subtitles = req
})
.catch(this.$showError)
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
try {
if (this.oldReq.items) {
this.updateLinks(this.oldReq.items)
} else {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.updateLinks(res.items)
}
} catch (e) {
this.$showError(e)
}
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)
},
methods: {
download () {
let url = `${this.$store.state.baseURL}/api/download`
url += this.req.url.slice(6)
return url
},
raw () {
return `${this.download()}?&inline=true`
},
back (event) {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
@ -118,30 +130,28 @@ export default {
if (this.hasPrevious) this.prev()
}
},
updateLinks () {
let pos = null
for (let i = 0; i < this.listing.items.length; i++) {
if (this.listing.items[i].name === this.req.name) {
pos = i
break
updateLinks (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].name !== this.req.name) {
continue
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(items[j].type)) {
this.previousLink = items[j].url
break
}
}
for (let j = i + 1; j < items.length; j++) {
if (mediaTypes.includes(items[j].type)) {
this.nextLink = items[j].url
break
}
}
}
if (pos === null) {
return
}
if (pos !== 0) {
this.previousLink = this.listing.items[pos - 1].url
}
if (pos !== this.listing.items.length - 1) {
this.nextLink = this.listing.items[pos + 1].url
}
},
allowEdit (event) {
return this.$store.state.user.allowEdit
}
}
}

View File

@ -10,11 +10,11 @@
</div>
<div class="card-action">
<button class="cancel flat"
<button class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
<button class="button button--flat"
@click="copy"
:disabled="$route.path === dest"
:aria-label="$t('buttons.copy')"
@ -26,7 +26,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import * as api from '@/utils/api'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
export default {
@ -40,7 +40,7 @@ export default {
},
computed: mapState(['req', 'selected']),
methods: {
copy: function (event) {
copy: async function (event) {
event.preventDefault()
buttons.loading('copy')
let items = []
@ -53,16 +53,14 @@ export default {
})
}
// Execute the promises.
api.copy(items)
.then(() => {
buttons.success('copy')
this.$router.push({ path: this.dest })
})
.catch(error => {
buttons.done('copy')
this.$showError(error)
})
try {
await api.copy(items)
buttons.success('copy')
this.$router.push({ path: this.dest })
} catch (e) {
buttons.done('copy')
this.$showError(e)
}
}
}
}

View File

@ -6,11 +6,11 @@
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
class="flat cancel"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
</div>
@ -19,61 +19,47 @@
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import { remove } from '@/utils/api'
import { files as api } from '@/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
export default {
name: 'delete',
computed: {
...mapGetters(['selectedCount']),
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected'])
},
methods: {
...mapMutations(['closeHovers']),
submit: function (event) {
submit: async function () {
this.closeHovers()
buttons.loading('delete')
// If we are not on a listing, delete the current
// opened file.
if (this.req.kind !== 'listing') {
remove(this.$route.path)
.then(() => {
buttons.success('delete')
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
})
.catch(error => {
buttons.done('delete')
this.$showError(error)
})
return
}
if (this.selectedCount === 0) {
// This shouldn't happen...
return
}
// Create the promises array and fill it with
// the delete request for every selected file.
let promises = []
for (let index of this.selected) {
promises.push(remove(this.req.items[index].url))
}
Promise.all(promises)
.then(() => {
try {
if (!this.isListing) {
await api.remove(this.$route.path)
buttons.success('delete')
this.$store.commit('setReload', true)
})
.catch(error => {
buttons.done('delete')
this.$store.commit('setReload', true)
this.$showError(error)
})
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
return
}
if (this.selectedCount === 0) {
return
}
let promises = []
for (let index of this.selected) {
promises.push(api.remove(this.req.items[index].url))
}
await Promise.all(promises)
buttons.success('delete')
this.$store.commit('setReload', true)
} catch (e) {
buttons.done('delete')
this.$showError(e)
if (this.isListing) this.$store.commit('setReload', true)
}
}
}
}

View File

@ -7,18 +7,20 @@
<div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p>
<button class="block cancel" @click="download('zip')" v-focus>zip</button>
<button class="block cancel" @click="download('tar')" v-focus>tar</button>
<button class="block cancel" @click="download('targz')" v-focus>tar.gz</button>
<button class="block cancel" @click="download('tarbz2')" v-focus>tar.bz2</button>
<button class="block cancel" @click="download('tarxz')" v-focus>tar.xz</button>
<button class="button button--block" @click="download('zip')" v-focus>zip</button>
<button class="button button--block" @click="download('tar')" v-focus>tar</button>
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button>
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button>
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button>
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button>
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api'
import { files as api } from '@/api'
export default {
name: 'download',

View File

@ -19,7 +19,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import * as api from '@/utils/api'
import { files } from '@/api'
export default {
name: 'file-list',
@ -35,7 +35,7 @@ export default {
}
},
computed: {
...mapState(['req']),
...mapState([ 'req' ]),
nav () {
return decodeURIComponent(this.current)
}
@ -51,7 +51,7 @@ export default {
// Otherwise, we must be on a preview or editor
// so we fetch the data from the previous directory.
api.fetch(url.removeLastDir(this.$route.path))
files.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.$showError)
},
@ -94,7 +94,7 @@ export default {
// content.
let uri = event.currentTarget.dataset.url
api.fetch(uri)
files.fetch(uri)
.then(this.fillOptions)
.catch(this.$showError)
},

View File

@ -21,7 +21,7 @@
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="flat"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
@ -29,6 +29,6 @@
</template>
<script>
export default {name: 'help'}
export default { name: 'help' }
</script>

View File

@ -7,27 +7,27 @@
<div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
<template v-if="dir() && selected.length === 0">
<template v-if="dir && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
</template>
<template v-if="!dir()">
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
<template v-if="!dir">
<p><strong>MD5: </strong><code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1: </strong><code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256: </strong><code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512: </strong><code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
</template>
</div>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="flat"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
@ -38,71 +38,44 @@
import {mapState, mapGetters} from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import * as api from '@/utils/api'
import { files as api } from '@/api'
export default {
name: 'info',
computed: {
...mapState(['req', 'selected']),
...mapGetters(['selectedCount'])
},
methods: {
...mapGetters(['selectedCount', 'isListing']),
humanSize: function () {
// If there are no files selected or this is not a listing
// show the human file size of the current request.
if (this.selectedCount === 0 || this.req.kind !== 'listing') {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size)
}
// Otherwise, sum the sizes of each selected file and returns
// its human form.
var sum = 0
let sum = 0
for (let i = 0; i < this.selectedCount; i++) {
sum += this.req.items[this.selected[i]].size
for (let selected of this.selected) {
sum += this.req.items[selected].size
}
return filesize(sum)
},
humanTime: function () {
// If there are no selected files, return the current request
// modified time.
if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow()
}
// Otherwise return the modified time of the first item
// that is selected since this should not appear when
// there is more than one file selected.
return moment(this.req.items[this.selected[0]]).fromNow()
},
name: function () {
// Return the name of the current opened file if there
// are no selected files.
if (this.selectedCount === 0) {
return this.req.name
}
// Otherwise, just return the name of the selected file.
// This field won't show when there is more than one
// file selected.
return this.req.items[this.selected[0]].name
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
},
dir: function () {
if (this.selectedCount > 1) {
// Don't show when multiple selected.
return true
}
if (this.selectedCount === 0) {
return this.req.isDir
}
return this.req.items[this.selected[0]].isDir
},
checksum: function (event, hash) {
// Gets the checksum of the current selected or
// opened file. Doesn't work for directories.
return this.selectedCount > 1 || (this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
}
},
methods: {
checksum: async function (event, algo) {
event.preventDefault()
let link
@ -113,9 +86,12 @@ export default {
link = this.$route.path
}
api.checksum(link, hash)
.then((hash) => { event.target.innerHTML = hash })
.catch(this.$showError)
try {
const hash = await api.checksum(link, algo)
event.target.innerHTML = hash
} catch (e) {
this.$showError(e)
}
}
}
}

View File

@ -9,11 +9,11 @@
</div>
<div class="card-action">
<button class="flat cancel"
<button class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
<button class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
@ -25,7 +25,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import * as api from '@/utils/api'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
export default {
@ -39,12 +39,11 @@ export default {
},
computed: mapState(['req', 'selected']),
methods: {
move: function (event) {
move: async function (event) {
event.preventDefault()
buttons.loading('move')
let items = []
// Create a new promise for each file.
for (let item of this.selected) {
items.push({
from: this.req.items[item].url,
@ -52,16 +51,14 @@ export default {
})
}
// Execute the promises.
api.move(items)
.then(() => {
buttons.success('move')
this.$router.push({ path: this.dest })
})
.catch(error => {
buttons.done('move')
this.$showError(error)
})
try {
api.move(items)
buttons.success('move')
this.$router.push({ path: this.dest })
} catch (e) {
buttons.done('move')
this.$showError(e)
}
event.preventDefault()
}

View File

@ -1,76 +0,0 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newArchetype') }}</p>
<input v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
</div>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import { removePrefix } from '@/utils/api'
export default {
name: 'new-archetype',
data: function () {
return {
name: '',
archetype: 'default'
}
},
methods: {
submit: function (event) {
event.preventDefault()
this.$store.commit('closeHovers')
this.new('/' + this.name, this.archetype)
.then((url) => {
this.$router.push({ path: url })
})
.catch(this.$showError)
},
new (url, type) {
url = removePrefix(url)
if (!url.endsWith('.md') && !url.endsWith('.markdown')) {
url += '.markdown'
}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
request.setRequestHeader('Archetype', encodeURIComponent(type))
request.onload = () => {
if (request.status === 200) {
resolve(request.getResponseHeader('Location'))
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
}
}
</script>

View File

@ -6,55 +6,66 @@
<div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p>
<input type="text" @keyup.enter="submit" v-model.trim="name" v-focus >
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus>
</div>
<div class="card-action">
<button class="cancel flat"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
@click="submit"
>{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import * as api from '@/utils/api'
export default {
name: 'new-dir',
data: function () {
data: function() {
return {
name: ''
}
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
},
methods: {
submit: function (event) {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
// Build the path of the new directory.
let uri = this.$route.path
if (this.$store.state.req.kind !== 'listing') {
let uri = this.isFiles ? this.$route.path + '/' : '/'
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
}
uri += this.name + '/'
uri = uri.replace('//', '/')
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(this.$showError)
try {
await api.post(uri)
this.$router.push({ path: uri })
} catch (e) {
this.$showError(e)
}
// Close the prompt
this.$store.commit('closeHovers')
}
}
}
};
</script>

View File

@ -6,56 +6,66 @@
<div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p>
<input v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
</div>
<div class="card-action">
<button class="cancel flat"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
:title="$t('buttons.create')"
>{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import * as api from '@/utils/api'
export default {
name: 'new-file',
data: function () {
data: function() {
return {
name: ''
}
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
},
methods: {
submit: function (event) {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
// Build the path of the new file.
let uri = this.$route.path
if (this.$store.state.req.kind !== 'listing') {
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/'
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
}
uri += this.name
uri = uri.replace('//', '/')
// Create the new file.
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(this.$showError)
try {
await api.post(uri)
this.$router.push({ path: uri })
} catch (e) {
this.$showError(e)
}
// Close the prompt.
this.$store.commit('closeHovers')
}
}
}
};
</script>

View File

@ -10,8 +10,6 @@
<move v-else-if="showMove"></move>
<copy v-else-if="showCopy"></copy>
<replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
<share v-else-if="show === 'share'"></share>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
@ -27,21 +25,16 @@ import Move from './Move'
import Copy from './Copy'
import NewFile from './NewFile'
import NewDir from './NewDir'
import NewArchetype from './NewArchetype'
import Replace from './Replace'
import Schedule from './Schedule'
import Share from './Share'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import * as api from '@/utils/api'
export default {
name: 'prompts',
components: {
Info,
Delete,
NewArchetype,
Schedule,
Rename,
Download,
Move,
@ -55,7 +48,6 @@ export default {
data: function () {
return {
pluginData: {
api,
buttons,
'store': this.$store,
'router': this.$router

View File

@ -6,16 +6,16 @@
<div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
</div>
<div class="card-action">
<button class="cancel flat"
<button class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
class="button button--flat"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
@ -24,9 +24,9 @@
</template>
<script>
import { mapState } from 'vuex'
import { mapState, mapGetters } from 'vuex'
import url from '@/utils/url'
import * as api from '@/utils/api'
import { files as api } from '@/api'
export default {
name: 'rename',
@ -38,14 +38,16 @@ export default {
created () {
this.name = this.oldName()
},
computed: mapState(['req', 'selected', 'selectedCount']),
computed: {
...mapState(['req', 'selected', 'selectedCount']),
...mapGetters(['isListing'])
},
methods: {
cancel: function (event) {
cancel: function () {
this.$store.commit('closeHovers')
},
oldName: function () {
// Get the current name of the file we are editing.
if (this.req.kind !== 'listing') {
if (!this.isListing) {
return this.req.name
}
@ -56,11 +58,11 @@ export default {
return this.req.items[this.selected[0]].name
},
submit: function (event) {
submit: async function () {
let oldLink = ''
let newLink = ''
if (this.req.kind !== 'listing') {
if (!this.isListing) {
oldLink = this.req.url
} else {
oldLink = this.req.items[this.selected[0]].url
@ -69,16 +71,17 @@ export default {
this.name = encodeURIComponent(this.name)
newLink = url.removeLastDir(oldLink) + '/' + this.name
api.move([{ from: oldLink, to: newLink }])
.then(() => {
if (this.req.kind !== 'listing') {
this.$router.push({ path: newLink })
return
}
this.$store.commit('setReload', true)
}).catch(error => {
this.$showError(error)
})
try {
await api.move([{ from: oldLink, to: newLink }])
if (!this.isListing) {
this.$router.push({ path: newLink })
return
}
this.$store.commit('setReload', true)
} catch (e) {
this.$showError(e)
}
this.$store.commit('closeHovers')
}

View File

@ -9,11 +9,11 @@
</div>
<div class="card-action">
<button class="flat cancel"
<button class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
<button class="button button--flat button--red"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>

View File

@ -1,47 +0,0 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.schedule') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.scheduleMessage') }}</p>
<input v-focus type="datetime-local" v-model="date">
</div>
<div class="card-action">
<button class="cancel flat"
@click="close"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="falt"
@click="submit"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'schedule',
data: function () {
return {
date: ''
}
},
methods: {
close () {
this.$store.commit('closeHovers')
},
submit: function (event) {
event.preventDefault()
if (this.date === '') return
this.close()
this.$store.commit('setSchedule', this.date)
document.getElementById('save-button').click()
}
}
}
</script>

View File

@ -12,7 +12,7 @@
<li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</a>
@ -49,7 +49,7 @@
</div>
<div class="card-action">
<button class="flat"
<button class="button button--flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
@ -58,8 +58,9 @@
</template>
<script>
import { mapState } from 'vuex'
import { getShare, deleteShare, share } from '@/utils/api'
import { mapState, mapGetters } from 'vuex'
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import moment from 'moment'
import Clipboard from 'clipboard'
@ -75,10 +76,10 @@ export default {
}
},
computed: {
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
...mapState([ 'req', 'selected', 'selectedCount' ]),
...mapGetters([ 'isListing' ]),
url () {
// Get the current name of the file we are editing.
if (this.req.kind !== 'listing') {
if (!this.isListing) {
return this.$route.path
}
@ -90,27 +91,25 @@ export default {
return this.req.items[this.selected[0]].url
}
},
beforeMount () {
getShare(this.url)
.then(links => {
this.links = links
this.sort()
async beforeMount () {
try {
const links = await api.get(this.url)
this.links = links
this.sort()
for (let link of this.links) {
if (!link.expires) {
this.hasPermanent = true
break
}
for (let link of this.links) {
if (link.expire === 0) {
this.hasPermanent = true
break
}
})
.catch(error => {
if (error === 404) return
this.$showError(error)
})
}
} catch (e) {
this.$showError(e)
}
},
mounted () {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', (e) => {
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
},
@ -118,42 +117,48 @@ export default {
this.clip.destroy()
},
methods: {
submit: function (event) {
submit: async function () {
if (!this.time) return
share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() })
.catch(this.$showError)
try {
const res = await api.create(this.url, this.time, this.unit)
this.links.push(res)
this.sort()
} catch (e) {
this.$showError(e)
}
},
getPermalink (event) {
share(this.url)
.then(result => {
this.links.push(result)
this.sort()
this.hasPermanent = true
})
.catch(this.$showError)
getPermalink: async function () {
try {
const res = await api.create(this.url)
this.links.push(res)
this.sort()
this.hasPermanent = true
} catch (e) {
this.$showError(e)
}
},
deleteLink (event, link) {
deleteLink: async function (event, link) {
event.preventDefault()
deleteShare(link.hash)
.then(() => {
if (!link.expires) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
})
.catch(this.$showError)
try {
await api.remove(link.hash)
if (link.expire === 0) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
} catch (e) {
this.$showError(e)
}
},
humanTime (time) {
return moment(time).fromNow()
return moment(time * 1000).fromNow()
},
buildLink (hash) {
return `${window.location.origin}${this.baseURL}/share/${hash}`
return `${window.location.origin}${baseURL}/share/${hash}`
},
sort () {
this.links = this.links.sort((a, b) => {
if (!a.expires) return -1
if (!b.expires) return 1
return new Date(a.expireDate) - new Date(b.expireDate)
if (a.expire === 0) return -1
if (b.expire === 0) return 1
return new Date(a.expire) - new Date(b.expire)
})
}
}

View File

@ -0,0 +1,24 @@
<template>
<div>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input class="input input--block" type="text" v-model.trim="raw">
</div>
</template>
<script>
export default {
name: 'permissions',
props: ['commands'],
computed: {
raw: {
get () {
return this.commands.join(' ')
},
set (value) {
this.$emit('update:commands', value.split(' '))
}
}
}
}
</script>

View File

@ -1,28 +1,29 @@
<template>
<select v-on:change="change" :value="selected">
<select v-on:change="change" :value="locale">
<option value="ar">{{ $t('languages.ar') }}</option>
<option value="de">{{ $t('languages.de') }}</option>
<option value="en">{{ $t('languages.en') }}</option>
<option value="it">{{ $t('languages.it') }}</option>
<option value="es">{{ $t('languages.es') }}</option>
<option value="fr">{{ $t('languages.fr') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option>
<option value="pt-br">{{ $t('languages.ptBR') }}</option>
<option value="it">{{ $t('languages.it') }}</option>
<option value="ja">{{ $t('languages.ja') }}</option>
<option value="ko">{{ $t('languages.ko') }}</option>
<option value="pl">{{ $t('languages.pl') }}</option>
<option value="pt-br">{{ $t('languages.ptBR') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option>
<option value="ru">{{ $t('languages.ru') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
<option value="es">{{ $t('languages.es') }}</option>
<option value="de">{{ $t('languages.de') }}</option>
<option value="ru">{{ $t('languages.ru') }}</option>
<option value="pl">{{ $t('languages.pl') }}</option>
<option value="ko">{{ $t('languages.ko') }}</option>
</select>
</template>
<script>
export default {
name: 'languages',
props: [ 'selected' ],
props: [ 'locale' ],
methods: {
change (event) {
this.$emit('update:selected', event.target.value)
this.$emit('update:locale', event.target.value)
}
}
}

View File

@ -0,0 +1,39 @@
<template>
<div>
<h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
</div>
</template>
<script>
export default {
name: 'permissions',
props: ['perm'],
computed: {
admin: {
get () {
return this.perm.admin
},
set (value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true
}
}
this.perm.admin = value
}
}
}
}
</script>

View File

@ -0,0 +1,57 @@
<template>
<form class="rules small">
<div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex"><label>Regex</label>
<input type="checkbox" v-model="rule.allow"><label>Allow</label>
<input
@keypress.enter.prevent
type="text"
v-if="rule.regex"
v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')" />
<input
@keypress.enter.prevent
type="text"
v-else
v-model="rule.path"
:placeholder="$t('settings.insertPath')" />
<button class="button button--red" @click="remove($event, index)">-</button>
</div>
<div>
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button>
</div>
</form>
</template>
<script>
export default {
name: 'rules-textarea',
props: ['rules'],
methods: {
remove (event, index) {
event.preventDefault()
let rules = [ ...this.rules ]
rules.splice(index, 1)
this.$emit('update:rules', [ ...rules ])
},
create (event) {
event.preventDefault()
this.$emit('update:rules', [
...this.rules,
{
allow: true,
path: '',
regex: false,
regexp: {
raw: ''
}
}
])
}
}
}
</script>

View File

@ -0,0 +1,65 @@
<template>
<div>
<p v-if="!isDefault">
<label for="username">{{ $t('settings.username') }}</label>
<input class="input input--block" type="text" v-model="user.username" id="username">
</p>
<p v-if="!isDefault">
<label for="password">{{ $t('settings.password') }}</label>
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password">
</p>
<p>
<label for="scope">{{ $t('settings.scope') }}</label>
<input class="input input--block" type="text" v-model="user.scope" id="scope">
</p>
<p>
<label for="locale">{{ $t('settings.language') }}</label>
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages>
</p>
<p v-if="!isDefault">
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
</p>
<permissions :perm.sync="user.perm" />
<commands :commands.sync="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.rulesHelp') }}</p>
<rules :rules.sync="user.rules" />
</div>
</div>
</template>
<script>
import Languages from './Languages'
import Rules from './Rules'
import Permissions from './Permissions'
import Commands from './Commands'
export default {
name: 'user',
components: {
Permissions,
Languages,
Rules,
Commands
},
props: [ 'user', 'isNew', 'isDefault' ],
computed: {
passwordPlaceholder () {
return this.isNew ? '' : this.$t('settings.avoidChanges')
}
},
watch: {
'user.perm.admin': function () {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}
}
}
</script>

55
src/css/_buttons.css Normal file
View File

@ -0,0 +1,55 @@
.button {
outline: 0;
border: 0;
padding: .5em 1em;
border-radius: .1em;
cursor: pointer;
background: var(--blue);
color: white;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
transition: .1s ease all;
}
.button:hover {
background-color: var(--dark-blue);
}
.button--block {
margin: 0 0 0.5em;
display: block;
width: 100%;
}
.button--red {
background: var(--red);
}
.button--red:hover {
background: var(--dark-red);
}
.button--flat {
color: var(--dark-blue);
background: transparent;
box-shadow: 0 0 0;
border: 0;
text-transform: uppercase;
}
.button--flat:hover {
background: var(--moon-grey);
}
.button--flat.button--red {
color: var(--dark-red);
}
.button--flat.button--grey {
color: #6f6f6f;
}
.button[disabled] {
opacity: .5;
cursor: not-allowed;
}

35
src/css/_inputs.css Normal file
View File

@ -0,0 +1,35 @@
.input {
border-radius: .1em;
padding: .5em 1em;
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
transition: .2s ease all;
color: #333;
margin: 0;
}
.input:hover,
.input:focus {
border-color: rgba(0, 0, 0, 0.2);
}
.input--block {
margin-bottom: .5em;
display: block;
width: 100%;
}
.input--textarea {
line-height: 1.15;
font-family: monospace;
min-height: 10em;
resize: vertical;
}
.input--red {
background: #fcd0cd;
}
.input--green {
background: #c9f2da;
}

29
src/css/_share.css Normal file
View File

@ -0,0 +1,29 @@
.share__box {
text-align: center;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
width: 90%;
max-width: 25em;
margin: 6em auto;
}
.share__box__download {
width: 100%;
padding: 1em;
cursor: pointer;
background: #ffffff;
color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.share__box__info {
padding: 2em 3em;
}
.share__box__title {
margin-top: .2em;
overflow: hidden;
text-overflow: ellipsis;
}

53
src/css/_shell.css Normal file
View File

@ -0,0 +1,53 @@
.shell {
position: fixed;
bottom: 0;
left: 0;
height: 25em;
max-height: calc(100% - 4em);
background: white;
color: #212121;
z-index: 9999;
width: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform;
}
.shell__result {
display: flex;
padding: 0.5em;
align-items: flex-start;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.shell--hidden {
transform: translateY(105%);
}
.shell__result--hidden {
opacity: 0;
}
.shell__text,
.shell__prompt,
.shell__prompt i {
font-size: inherit;
}
.shell__prompt {
width: 1.2rem;
}
.shell__prompt i {
color: var(--blue);
}
.shell__text {
margin: 0;
font-family: inherit;
white-space: pre-wrap;
width: 100%;
}

7
src/css/_variables.css Normal file
View File

@ -0,0 +1,7 @@
:root {
--blue: #2196f3;
--dark-blue: #1E88E5;
--red: #F44336;
--dark-red: #D32F2F;
--moon-grey: #f2f2f2;
}

View File

@ -29,94 +29,6 @@ video {
width: 100%;
}
pre {
padding: 1em;
border: 1px solid #e6e6e6;
border-radius: 0.5em;
background-color: #f5f5f5;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
input,
button {
outline: 0 !important;
}
input[type="submit"],
button {
border: 0;
padding: .5em 1em;
margin-left: .5em;
border-radius: .1em;
cursor: pointer;
background: #2196f3;
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
transition: .1s ease all;
}
input[type="submit"]:hover,
button:hover {
background-color: #1E88E5;
}
input[type="submit"].block,
button.block {
display: block;
width: 100%;
margin: 0 0 1em;
}
button.delete {
background: #F44336;
}
button.delete:hover {
background: #D32F2F;
}
button.cancel {
background-color: #ECEFF1;
color: #37474F;
}
button.cancel:hover {
background-color: #e9eaeb;
}
button.flat,
input[type="submit"].flat {
color: #1E88E5;
background: transparent;
box-shadow: 0 0 0;
border: 0;
margin-left: 0;
text-transform: uppercase;
}
button.flat:hover,
input[type="submit"].flat:hover {
background: rgba(0,0,0,0.05)
}
button.flat.delete {
color: #F44336;
}
button.flat.cancel {
color: #ccc;
}
button.flat[disabled] {
color: #ccc;
cursor: not-allowed;
}
.mobile-only {
display: none !important;
}

View File

@ -7,58 +7,6 @@ a {
color: inherit
}
select,
textarea,
input[type="text"],
input[type="password"] {
padding: 0.5em 0;
line-height: 1;
display: block;
border: 0;
border-bottom: 1px solid #dddddd;
transition: .2s ease border;
width: 100%;
background: transparent;
}
textarea {
line-height: 1.15;
padding: .5em;
border: 1px solid #ddd;
font-family: monospace;
min-height: 10em;
resize: none;
border-radius: 2px;
}
.dashboard #locale,
.dashboard #username,
.dashboard #password,
.dashboard #scope {
max-width: 18em;
}
.dashboard #locale {
margin-top: .5em;
}
textarea:focus,
textarea:hover,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="text"]:hover,
input[type="password"]:hover {
border-color: #2979ff;
}
input.red {
border-color: red;
}
input.green {
border-color: green;
}
.dashboard p label {
margin-bottom: .2em;
display: block;
@ -98,7 +46,7 @@ p code {
}
.dashboard #nav li.active {
border-color: #2196f3
border-color: var(--blue)
}
.dashboard #nav i {
@ -241,7 +189,7 @@ table tr>*:last-child {
}
.card#share ul li a {
color: #2196F3;
color: var(--blue);
cursor: pointer;
margin-right: auto;
}
@ -310,7 +258,7 @@ table tr>*:last-child {
}
.file-list li[aria-selected=true] {
background: #2196f3 !important;
background: var(--blue) !important;
color: #fff !important;
transition: .1s ease all;
}

View File

@ -1,184 +0,0 @@
@import "~codemirror/lib/codemirror.css";
@import "~codemirror/theme/ttcn.css";
#editor {
max-width: 800px;
margin: 0 auto;
}
#editor .CodeMirror {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
margin: 2em 0;
border-radius: .5em;
}
#editor h2 {
color: rgba(0, 0, 0, 0.3);
font-weight: 500;
}
.CodeMirror {
height: auto;
}
.markdown .CodeMirror {
padding: .75em;
}
.cm-s-markdown .CodeMirror-gutter {
border-right: 1px solid #eff3f5;
padding-right: 5px;
margin-right: 15px;
min-width: 2.5em;
padding-bottom: 30px;
}
.cm-s-markdown .CodeMirror-cursor {
border-right: 2px solid #667880;
}
.cm-s-markdown .CodeMirror-lines {
margin: 0;
}
.cm-s-markdown {
color: #3D494E;
}
.cm-s-markdown span.cm-header {
color: #3D494E;
font-weight: bold;
}
.cm-s-markdown span.cm-variable-2 {
color: #3D494E;
}
.cm-s-markdown span.cm-meta {
color: #516066;
}
.cm-s-markdown span.cm-hr {
color: #516066;
}
.cm-s-markdown span.cm-comment {
color: #868f93;
}
.cm-s-markdown span.cm-qualifier {
color: #868f93;
}
.cm-s-markdown span.cm-number {
color: #197987;
}
.cm-s-markdown span.cm-variable {
color: #197987;
}
.cm-s-markdown span.cm-builtin {
color: #197987;
}
.cm-s-markdown span.cm-link {
color: #197987;
text-decoration: underline;
}
.cm-s-markdown span.cm-tag {
color: #197987;
}
.cm-s-markdown span.cm-string {
color: #48abb9;
}
.cm-s-markdown span.cm-string-2 {
color: #48abb9;
}
.cm-s-markdown span.cm-quote {
color: #48abb9;
}
.cm-s-markdown span.cm-atom {
color: #48abb9;
}
.cm-s-markdown span.cm-property {
color: #82a367;
}
.cm-s-markdown span.cm-operator {
color: #82a367;
}
.cm-s-markdown span.cm-variable-3 {
color: #82a367;
}
.cm-s-markdown span.cm-attribute {
color: #90bb74;
}
.cm-s-markdown span.cm-def {
color: #90bb74;
}
.cm-s-markdown span.cm-keyword {
color: #ec6c45;
}
.cm-s-markdown span.cm-bracket {
color: #ec6c45;
}
.cm-s-markdown span.cm-error {
color: #e45346;
}
.cm-s-markdown span.cm-em {
font-style: italic;
}
.cm-s-markdown span.cm-strong {
font-weight: bold;
}
.cm-s-markdown .cm-header-1 {
font-size: 200%;
line-height: 200%;
}
.cm-s-markdown .cm-header-2 {
font-size: 160%;
line-height: 160%;
}
.cm-s-markdown .cm-header-3 {
font-size: 125%;
line-height: 125%;
}
.cm-s-markdown .cm-header-4 {
font-size: 110%;
line-height: 110%;
}
.cm-s-markdown .cm-comment {
background: rgba(0, 0, 0, .05);
border-radius: 2px;
}
.cm-s-markdown .cm-link {
color: #7f8c8d;
}
.cm-s-markdown .cm-url {
color: #aab2b3;
}
.cm-s-markdown .cm-strikethrough {
text-decoration: line-through;
}

View File

@ -110,32 +110,4 @@
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(../assets/fonts/material/icons.eot);
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../assets/fonts/material/icons.woff2) format('woff2'),
url(../assets/fonts/material/icons.ttf) format('truetype');
}
.prompt .file-list ul li:before,
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
}
@import "~material-design-icons/iconfont/material-icons.css";

View File

@ -238,7 +238,7 @@ header .search-button {
}
#search .boxes>div>div {
background: #2196F3;
background: var(--blue);
color: #fff;
text-align: center;
width: 10em;

View File

@ -120,7 +120,7 @@
}
#listing .item[aria-selected=true] {
background: #2196f3 !important;
background: var(--blue) !important;
color: #fff !important;
}
@ -228,7 +228,7 @@
left: 0;
z-index: 99999;
width: 100%;
background-color: #2196f3;
background-color: var(--blue);
height: 4em;
display: none;
padding: 0.5em 0.5em 0.5em 1em;

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