Repository: developit/dropfox
Branch: master
Commit: b18a2cfc7075
Files: 22
Total size: 30.4 KB
Directory structure:
gitextract_bfsdldih/
├── .babelrc
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── electron/
│ ├── backend.js
│ ├── index.js
│ ├── menu.js
│ └── package.json
├── package.json
├── src/
│ ├── assets/
│ │ ├── icon.icns
│ │ └── oauth_receiver.html
│ ├── components/
│ │ ├── app.js
│ │ ├── file-list.js
│ │ ├── path-bar.js
│ │ └── sidebar.js
│ ├── index.js
│ ├── lib/
│ │ ├── dropbox-client.js
│ │ ├── menu.js
│ │ └── remote-require.js
│ └── styles/
│ └── index.less
└── webpack.config.babel.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"sourceMaps": true,
"presets": [
"es2015-minimal",
"stage-0"
],
"plugins": [
"add-module-exports",
["transform-decorators-legacy"],
["transform-react-jsx", { "pragma": "h" }]
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .gitignore
================================================
node_modules
/npm-debug.log
/dist
/build
/app
/design
/.env
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) Jason Miller <jason@developit.ca> (http://jasonformat.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Dropfox
A dropbox client powered by [Preact], [Electron] and [Photon].
> ### [Download Dropfox ➞](https://github.com/developit/dropfox/releases)
<img src="http://i.imgur.com/fN1PmUN.png" width="717">
> **Note:** building the app requires a Dropbox API Key, specified as `DROPBOX_API_KEY` env var.
>
> If you need a key, [generate one here](https://www.dropbox.com/developers/apps/).
## Installation
```sh
npm install
```
### Run for Development
Runs a local copy of Electron (via electron-prebuilt), rendering the app with Live-Reload / [HMR] via [webpack-dev-server].
> **Note:** you may need to reload _(Cmd/Ctrl + R)_ after the initial Webpack build completes.
```sh
npm start
```
### Build
To build the app for OS X, Linux, and Windows, using [electron-packager]:
```sh
npm run build
```
### Platform-Specific Builds
You can also build the codebase, and then package it only for a given platform:
```sh
# build the electron & web source:
npm run build:all
# generate the package for your platform(s):
npm run build:electron:osx
npm run build:electron:linux
npm run build:electron:win
```
## License
MIT © [Jason Miller](http://jasonformat.com)
[webpack-dev-server]: https://webpack.github.io/docs/webpack-dev-server.html
[HMR]: https://webpack.github.io/docs/hot-module-replacement.html
[preact]: https://github.com/developit/preact
[electron]: https://github.com/atom/electron
[photon]: https://github.com/connors/photon
[electron-packager]: https://github.com/maxogden/electron-packager
================================================
FILE: electron/backend.js
================================================
import fs from 'fs';
import request from 'request';
export function upload(path, url, callback) {
function done(err) {
if (callback) callback(err);
callback = null;
}
fs.createReadStream(path)
.on('error', done)
.pipe(
request.put(url, done)
.on('error', done)
.on('finish', () => done() )
)
.on('finish', () => done() );
};
================================================
FILE: electron/index.js
================================================
import { parse as parseUrl } from 'url';
import app from 'app';
import BrowserWindow from 'browser-window';
import menu from './menu';
const HOST = `localhost:${process.env.PORT || 19998}`;
const DEV = process.env.NODE_ENV==='development';
// adds debug features like hotkeys for triggering dev tools and reload
if (DEV) {
try { require('electron-debug')(); }catch(err){}
}
// prevent window being garbage collected
let mainWindow;
app.on('ready', () => {
mainWindow = createMainWindow();
overrideWindowOpen();
});
function createMainWindow() {
const win = new BrowserWindow({
width: DEV ? 1200 : 800,
height: DEV ? 600 : 500,
minWidth: 500,
minHeight: 200,
webgl: false,
acceptFirstMouse: true,
titleBarStyle: 'hidden',
show: false
});
menu(win);
win.on('closed', () => {
mainWindow = null;
});
if (DEV) {
win.loadURL(`http://${HOST}/`);
win.toggleDevTools();
}
else {
win.loadURL(`file://${__dirname}/web/index.html`);
}
setTimeout( () => win.show(), 150);
return win;
}
function overrideWindowOpen() {
mainWindow.webContents.on('new-window', (e, url, name, disp, options) => {
let host = parseUrl(url).host;
if (host && host!==HOST) {
Object.assign(options, {
width: 400,
height: 500,
center: true,
frame: true,
resizable: false,
title: `Loading ${host}...`,
titleBarStyle: 'visible',
alwaysOnTop: true,
useContentSize: true,
skipTaskbar: true,
nodeIntegration: false,
webPreferences: {
'web-security': true
}
});
}
else {
console.warn(`Allowing node integration for URL: ${url}`);
}
});
}
// quit if all windows are closed
app.on('window-all-closed', () => app.quit() );
/*
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate-with-no-open-windows', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
*/
================================================
FILE: electron/menu.js
================================================
import { openExternal } from 'shell';
import app from 'app';
import Menu from 'menu';
const MAC = process.platform==='darwin';
const DEV = process.env.NODE_ENV==='dev';
const MENU = [];
export default win => {
Menu.setApplicationMenu(
Menu.buildFromTemplate(
filterDev(MENU)
)
);
};
let filterDev = menu => menu.filter( item => (DEV || item.devOnly!==true)).map( item => {
let d = Object.assign({}, item);
if (d.submenu) d.submenu = filterDev(d.submenu);
return d;
});
MENU.push(
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo'
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo'
},
{
type: 'separator'
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut'
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
},
]
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow)
focusedWindow.reload();
}
},
{
label: 'Toggle Full Screen',
accelerator: MAC ? 'Ctrl+Command+F' : 'F11',
click(item, focusedWindow) {
if (focusedWindow)
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
{
devOnly: true,
label: 'Toggle Developer Tools',
accelerator: MAC ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
if (focusedWindow)
focusedWindow.toggleDevTools();
}
},
]
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
},
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Developer Website',
click: () => openExternal('http://jasonformat.com')
},
]
}
);
if (MAC) {
let appName = app.getName();
MENU.unshift({
label: appName,
submenu: [
{
label: `About ${appName}`,
role: 'about'
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: `Hide ${appName}`,
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
});
// Window menu.
MENU[3].submenu.push(
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
);
}
================================================
FILE: electron/package.json
================================================
{
"name": "Dropfox",
"productName": "Dropfox",
"version": "1.2.0",
"description": "Dropbox client, powered by Preact & Electron.",
"main": "index.js",
"author": {
"name": "Jason Miller",
"email": "jason@developit.ca",
"url": "http://jasonformat.com"
},
"electronVersion": "0.37.5",
"files": [
"web/index.html",
"web/bundle.js",
"web/style.css"
],
"dependencies": {
"request": "^2.70.0",
"tmp": "0.0.28"
}
}
================================================
FILE: package.json
================================================
{
"name": "dropfox",
"productName": "Dropfox",
"version": "1.2.0",
"description": "Dropbox client, powered by Preact & Electron.",
"license": "MIT",
"repository": "developit/dropfox",
"author": "Jason Miller <jason@developit.ca> (http://jasonformat.com)",
"engines": {
"node": ">=4"
},
"electronVersion": "0.37.5",
"scripts": {
"test": "eslint src",
"start": "NODE_ENV=development npm-run-all build:transpilewrap --parallel dev thenelectron",
"dev": "webpack-dev-server --hot --inline --progress",
"thenelectron": "sleep 2; electron ./app",
"clean": "rm -rf app && rm -rf dist && mkdir -p app/web/assets",
"build": "npm-run-all build:all build:electron-all",
"build:test": "npm-run-all build:all build:electron:osx && ./dist/${npm_package_productName}-darwin-x64/${npm_package_productName}.app/Contents/MacOS/Dropfox",
"build:all": "npm-run-all clean build:assets build:transpilewrap build:transpile build:packagejson build:install",
"build:assets": "ncp src/assets app/web/assets",
"build:packagejson": "ncp electron/package.json app/package.json",
"build:install": "cd app && npm i --production && cd ..",
"build:transpilewrap": "babel electron -s inline -d app",
"build:transpile": "NODE_ENV=production webpack -p",
"build:electron-all": "npm-run-all build:electron:*",
"build:electron:osx": "npm run build:electron -- --icon ./src/assets/icon.icns --platform=darwin && electron-installer-dmg ./dist/${npm_package_productName}-darwin-x64/${npm_package_productName}.app $npm_package_productName --out=./dist/${npm_package_productName}-darwin-x64 --icon=./src/assets/icon.icns --overwrite",
"build:electron:linux": "npm run build:electron -- --icon ./src/assets/icon.png --platform=linux",
"build:electron:win": "npm run build:electron -- --icon ./src/assets/icon.ico --platform=win32",
"build:electron": "electron-packager ./app --out=dist --overwrite --asar --prune --arch=all"
},
"dependencies": {
"decko": "^1.1.3",
"dropbox": "robertknight/dropbox-js#5568865764d9deab69836569a69d72d200c88293",
"neatime": "^1.0.0",
"photon": "connors/photon#v0.1.2-alpha",
"praline": "^0.3.1",
"preact": "^4.5.1",
"preact-photon": "^1.1.1",
"wildemitter": "^1.2.0"
},
"devDependencies": {
"autoprefixer": "^6.3.6",
"babel-cli": "^6.7.5",
"babel-core": "^6.7.6",
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-jsx": "^6.7.5",
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-minimal": "^1.1.0",
"babel-preset-stage-0": "^6.5.0",
"css-loader": "^0.23.1",
"electron-debug": "^0.6.0",
"electron-installer-dmg": "^0.1.0",
"electron-packager": "^6.0.1",
"electron-prebuilt": "^0.37.5",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.4",
"html-webpack-plugin": "^2.15.0",
"less": "^2.6.1",
"less-loader": "^2.2.1",
"ncp": "^2.0.0",
"npm-run-all": "^1.7.0",
"postcss-loader": "^0.8.2",
"raw-loader": "^0.5.1",
"redux": "^3.4.0",
"source-map-loader": "^0.1.5",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.14",
"webpack-dev-server": "^1.14.1"
}
}
================================================
FILE: src/assets/oauth_receiver.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.addEventListener('load', function() {
var opener = window.parent!==window.top ? window.parent : window.opener;
if (!opener) return;
try {
opener.postMessage(JSON.stringify({
_dropboxjs_oauth_info: location.href
}), location.origin);
window.close();
} catch (e) {}
});
</script>
</head>
<body>
<h1>Dropbox sign-in successful</h1>
<p>Please close this window.</p>
</body>
</html>
================================================
FILE: src/components/app.js
================================================
import { bind, memoize } from 'decko';
import { parallel } from 'praline';
import { Component, h, render } from 'preact';
import { Header, Title, Footer, Icon, Button, ButtonGroup } from 'preact-photon';
import Sidebar from './sidebar';
import PathBar from './path-bar';
import FileList from './file-list';
import { createMenu, createDomMenu, showMenu } from 'menu';
import dropbox from 'dropbox-client';
export default class App extends Component {
state = {
path: '/',
loading: false,
history: [],
files: []
};
componentDidMount() {
dropbox.init( err => {
if (err) return alert(err);
this.navigate('/');
});
}
@bind
navigate(to, { go=1 }={}) {
if (typeof to==='number') { go=to; to=''; }
let { path, history } = this.state;
if (to[0]=='/') {
path = to;
}
else {
path = path.replace(/\/+$/,'') + '/' + to.replace(/^\/+/,'');
}
while ( path !== (path = path.replace(/[^\/]+\/\.\.\//)) );
if (go) {
if (go===-1) {
history.pop();
path = history[history.length-1];
}
else {
history.push(path);
}
}
this.setState({ path, loading:true, history });
dropbox.readdir(path, (err, names, dir, files) => {
this.setState({ files, loading:false });
});
}
@bind
search(search) {
this.setState({ search, loading:true });
dropbox.search(this.state.path, search, (err, files) => {
this.setState({ files, loading:false });
});
}
@bind
handleSearch({ target:{value:search} }) {
if (search) {
this.search(search);
}
else {
this.navigate(0);
}
}
@bind
handleFile(e) {
let file = e.file || this.menuFile;
if (file.isFolder) {
return this.navigate(file.name, { go:1 });
}
else {
this.openFile(file);
}
}
openFile(file) {
this.setState({ loading:true });
dropbox.open(file.path, {
autoSync: true,
onUpload: () => {
console.log('File changed and uploaded. Reloading list.');
this.navigate(0);
}
}, (err, localPath) => {
this.setState({ loading:false });
if (err) return alert(String(err));
});
}
@bind
to(...args) {
return () => this.navigate(...args);
}
getActions() {
return this.actions || (this.actions = {
go: ::this.navigate,
to: ::this.to
});
}
onDragOver(e) {
e.dataTransfer.dropEffect = 'copy';
e.preventDefault();
e.stopPropagation();
return false;
}
@bind
onDrop(e) {
let { path } = this.state,
files = [].slice.call(e.dataTransfer.files);
this.setState({ loading:true });
parallel( files.map( f => cb => {
let basename = (f.path.match(/([^\/]+)\/?$/g) || [])[0] || '';
dropbox.upload(f.path, `${path}/${basename}`, cb);
}), (err, ...results) => {
this.navigate(0);
});
e.preventDefault();
e.stopPropagation();
return false;
}
@bind
showFilesMenu(e) {
let { left, top } = e.target.getBoundingClientRect();
showMenu(createMenu([
{ label: 'New File', click: () => this.newFile() },
{ label: 'New Folder', click: () => this.newDirectory() },
{ type: 'separator' }
])); // , left, top
}
newFile(name, path=this.state.path) {
// @TODO: prompt is not supported in Atom, use my custom modal.
if (!name) name = prompt('Enter a name for the new file:');
if (!name) return;
dropbox.writeFile(`${path}/${basename}`, '', err => {
if (err) console.error(err);
this.navigate(0);
});
}
newDirectory(name, path=this.state.path) {
// @TODO: prompt is not supported in Atom, use my custom modal.
if (!name) name = prompt('Enter a name for the new folder:');
if (!name) return;
dropbox.mkdir(`${path}/${basename}`, err => {
if (err) console.error(err);
this.navigate(0);
});
}
@bind
spawnContextMenu(e) {
let t = e.target;
if (e && e.button!==2) return;
while (!t.hasAttribute('contextmenu') && (t=t.parentNode));
let id = t.getAttribute('contextmenu'),
dom = document.getElementById(id),
menu = dom && createDomMenu(dom);
if (menu) {
this.menuFile = e.file;
setTimeout( () => showMenu(menu), 100);
}
}
no() {
alert('Nobody uses this button');
}
render({}, { files, path, search, loading }) {
let actions = this.getActions();
return (
<div id="app" class="window">
<Header>
<Title>Dropfox</Title>
<div class="toolbar-actions">
<ButtonGroup>
<Button icon="left-open-big" onClick={ this.to(-1) } />
<Button icon="right-open-big" onClick={ this.no } />
</ButtonGroup>
<PathBar path={path} go={this.navigate} />
<Button dropdown disabled={loading} class="pull-right" onMouseDown={this.showFilesMenu}>
<Icon name="cog" />
</Button>
<label class="toolbar-input pull-right">
<span class="icon icon-search"></span>
<input
class="form-control"
type="search"
placeholder="Search..."
value={ search }
onSearch={ this.handleSearch } />
</label>
</div>
</Header>
<div class="window-content">
<div class="pane-group">
<Sidebar path={path} actions={actions} />
<div class="pane" onDragOver={this.onDragOver} onDragEnter={this.onDragOver} onDrop={this.onDrop}>
<FileList
files={files}
action={this.handleFile}
onContextMenu={this.spawnContextMenu}
contextmenu="file-menu" />
</div>
</div>
</div>
<Footer>
<Icon name={ loading?'switch':'cloud' } class="pull-right square" />
<Title>
{ files.length.toLocaleString() + ' items' }
</Title>
</Footer>
<menu type="context" id="file-menu" style="display:none;">
<menuitem label="Open" onClick={ this.handleFile } />
<menuitem label="Download" disabled />
<menuitem label="Properties" disabled />
<menu label="Open with...">
<menuitem label="Atom" />
<menuitem label="Sublime Text" />
<menuitem label="TextWrangler" />
</menu>
</menu>
</div>
);
}
}
================================================
FILE: src/components/file-list.js
================================================
import { bind, memoize } from 'decko';
import { h, Component } from 'preact';
import neatime from 'neatime';
import { Icon } from 'preact-photon';
const time = memoize( str => neatime(new Date(str)).replace(/^(\d+[a-z])$/g,'$1 ago') );
export default class FileList extends Component {
@bind
handleKey(e) {
if (e.code==='ArrowUp' || e.keyCode===38) {
this.move(-1);
}
else if (e.code==='ArrowDown' || e.keyCode===40) {
this.move(1);
}
else if (e.code==='Enter' || e.keyCode===13) {
this.openSelected();
}
else {
return;
}
e.preventDefault();
return false;
}
openSelected() {
let { action } = this.props,
{ selected } = this.state;
if (action && selected) {
action({ file: selected });
}
}
move(delta) {
let { files } = this.props,
{ selected } = this.state,
index = files.indexOf(selected);
if (index===-1 && delta<0) index = files.length;
index += delta;
if (index>=0 && index<files.length) {
selected = files[index];
this.setState({ selected });
}
}
componentDidMount() {
addEventListener('keydown', this.handleKey);
}
componentWillUnmount() {
removeEventListener('keydown', this.handleKey);
}
componentDidUpdate() {
let selected = this.base && this.base.querySelector('.selected');
if (selected) selected.scrollIntoViewIfNeeded();
}
render({ files, action, ...props }, { selected }) {
return (
<div class="cotable file-list">
<table>
<thead>
<tr>
<th width="40"></th>
<th>Name</th>
<th width="80">Size</th>
<th width="100">Modified</th>
</tr>
</thead>
</table>
<table class="striped">
<tbody>{
files.map( file => (
<FileListItem
selected={selected && selected.path===file.path}
file={file}
onAction={action}
onSelect={this.linkState('selected', 'file')}
{...props}
/>
))
}</tbody>
</table>
</div>
);
}
}
const FileListItem = ({ showThumbs=false, file, selected, onContextMenu, onSelect, onAction, ...props }) => (
<tr {...props} file={file} class={{selected}} onContextMenu={ fileProxy(file, onContextMenu) } onMouseDown={ fileProxy(file, onSelect) } onDblClick={ fileProxy(file, onAction) }>
<td width="40">{ showThumbs && file.hasThumbnail ? (
<div style={"width:1.8em; height:1.4em; background:url("+dropbox.thumbnailUrl(file.path)+") center/contain;"} />
) : (
<Icon class="square" name={ file.isFile ? 'doc-text-inv' : 'folder' } />
) }</td>
<td>{ file.name }</td>
<td width="80">{ file.humanSize }</td>
<td width="100">{ time(file.modifiedAt) }</td>
</tr>
);
function fileProxy(file, fn) {
return e => {
e.file = file;
if (fn) return fn(e);
};
}
================================================
FILE: src/components/path-bar.js
================================================
import { h, Component } from 'preact';
import { ButtonGroup, Button } from 'preact-photon';
const EXISTS = x => x;
export default ({ path, go }) => {
let parts = path.split('/').filter(EXISTS);
return (
<ButtonGroup>
<Button icon="home" onClick={ () => go('/') }>Home</Button>
{ parts.map( (dir, i) => (
<Button onClick={ () => go('/'+parts.slice(0,i+1).join('/')) }>{ dir }</Button>
) ) }
</ButtonGroup>
);
};
================================================
FILE: src/components/sidebar.js
================================================
import { h, Component } from 'preact';
import { NavGroup } from 'preact-photon';
export default class Sidebar extends Component {
// shouldComponentUpdate() {
// return false;
// }
paths = [
{ path:'/', icon:'home', label:'Home' },
{ path:'/Photos', icon:'picture' },
{ path:'/Music', icon:'note-beamed' },
{ path:'/Public', icon:'globe' },
{ path:'/Apps', icon:'cloud' }
];
render({ path, actions }) {
return (
<div class="pane pane-sm sidebar">
<NavGroup>
<NavGroup.Title>Places</NavGroup.Title>
{ this.paths.map( item => (
<SidebarItem {...item} actions={actions} active={ path===item.path } />
)) }
</NavGroup>
</div>
);
}
}
const SidebarItem = ({ active, icon, path, label, actions:{ to }, children }) => {
return <NavGroup.Item icon={ icon } onclick={ to(path) } class={{ active }}>{ children || label || path.replace('/','') }</NavGroup.Item>
};
================================================
FILE: src/index.js
================================================
import { h, render } from 'preact';
import './styles/index.less';
let root;
function init() {
let App = require('./components/app');
root = render(<App />, document.body, root);
}
init();
if (module.hot) module.hot.accept('./components/app', () => requestAnimationFrame(init) );
================================================
FILE: src/lib/dropbox-client.js
================================================
import Emitter from 'wildemitter';
import { debounce } from 'decko';
import { Client, AuthDriver } from 'dropbox';
import remoteRequire from 'remote-require';
const request = remoteRequire('request');
const fs = remoteRequire('fs');
const tmp = remoteRequire('tmp');
const shell = remoteRequire('shell');
const dropbox = new Client({
key: API_KEY
});
const CLEANUPS = {};
const NOOP = ()=>{};
export default dropbox;
Object.assign(dropbox, new Emitter());
Object.assign(dropbox, Emitter.prototype);
dropbox.authDriver(new AuthDriver.Popup({
receiverUrl: 'https://dropfox.firebaseapp.com/dropbox/oauth_receiver.html'
//receiverUrl: location.href.replace(/[^/]+$/,'') + 'assets/oauth_receiver.html'
}));
export function init(callback=NOOP) {
dropbox.authenticate({ interactive: false }, err => {
if (err) return callback(err);
if (dropbox.isAuthenticated()) {
dropbox.emit('init');
return callback();
}
dropbox.authenticate( err => {
if (err) return callback(err);
dropbox.emit('init');
callback();
});
});
}
export function stream(path, callback) {
path = path.replace(/^\//,'');
let url = `${dropbox._urls.getFile}/${path}?access_token=${dropbox.credentials().token}`,
basename = (path.match(/([^\/]+)\/?$/g) || [])[0] || '',
error, localPath,
done = debounce(() => callback ? callback(error, localPath) : (callback = null));
tmp.dir( (err, target, fd, cleanup) => {
localPath = target + '/' + basename;
CLEANUPS[localPath] = cleanup;
request.get(url)
.on('error', err => { error = err; done(); })
.pipe(
fs.createWriteStream(localPath)
.on('error', err => { error = err; done(); })
.on('finish', done)
);
});
}
export function open(path, options, callback) {
if (typeof options==='function') {
[callback, options] = [options, callback];
}
options = options || {};
stream(path, (err, localPath) => {
if (!err) {
shell.openItem(localPath);
if (options.autoSync) {
watchAndUpload(localPath, path, options.onSync || options.onUpload);
}
}
callback(err, localPath);
});
}
function watchAndUpload(localPath, path, onUpload) {
let start = Date.now();
fs.watch(localPath, {
persistent: false
}, debounce(1000, changeType => {
if (Date.now()-start < 3000) return;
// console.log('changed', changeType);
upload(localPath, path, onUpload);
}));
}
let backend = remoteRequire('./backend');
export function upload(localPath, path, callback) {
let url = `${dropbox._urls.putFile}/${path}?access_token=${dropbox.credentials().token}`;
backend.upload(localPath, url, err => {
callback(err);
});
}
Object.assign(dropbox, { init, stream, open, upload });
// console.log(window.dropbox = dropbox);
================================================
FILE: src/lib/menu.js
================================================
import remote from 'remote';
const Menu = remote.require('menu');
const MenuItem = remote.require('menu-item');
export function createMenu(template) {
let menu = Menu.buildFromTemplate(template);
return menu;
}
export function showMenu(menu, x, y) {
menu.popup(remote.getCurrentWindow(), x, y);
}
export function createDomMenu(element) {
let menu = new Menu();
[].forEach.call(element.children, child => {
let item = {};
if (child.nodeName==='separator') {
item.type = 'separator';
}
else {
item.label = child.textContent;
for (let i in child.attributes) {
item[child.attributes[i].name] = child.attributes[i].value;
}
if (child.nodeName==='MENU') {
item.type = 'submenu';
item.submenu = createDomMenu(child);
}
else {
item.click = () => { console.log('click'); child.click(); };
}
}
menu.append(new MenuItem(item));
});
return menu;
}
================================================
FILE: src/lib/remote-require.js
================================================
import remote from 'remote';
// remote.require(), patched to work in both dev and prod mode:
export default function remoteRequire(module) {
if (process.env.NODE_ENV!=='development') {
module = module.replace(/^\.\//, __dirname+'/../');
}
return remote.require(module);
}
================================================
FILE: src/styles/index.less
================================================
@import (less) '~photon/dist/css/photon.css';
#app {
.toolbar {
-webkit-app-region: drag;
.form-control {
-webkit-app-region: no-drag;
}
}
.sidebar {
flex: 0;
}
}
tr.selected,
.table-striped tr:nth-child(even).selected {
color: #fff;
background-color: #116cd6;
}
.file-list {
th, td {
padding: 2px 8px;
}
}
// fix <ButtonGroup><Button /></ButtonGroup>
.btn-group > .btn:first-child:last-child {
border-radius: 4px;
}
// normalize search inputs (but keep the cancel "X" button):
input[type="search"] {
-webkit-appearance: textfield;
&::-webkit-search-cancel-button {
-webkit-appearance: searchfield-cancel-button;
opacity: 1;
}
}
.toolbar-input {
position: relative;
overflow: visible;
margin: -1px 0 0;
padding: 0;
.icon {
position: absolute;
left: 10px;
top: 4px;
z-index: 1;
opacity: 0.5; /* ~matches placeholder */
}
.form-control {
padding: 3px 4px 1px 24px;
border-radius: 12px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.4),
0 0 0 7px rgba(109,179,253,0);
&:focus {
box-shadow: inset 0 1px 2px rgba(0,0,0,0.4),
0 0 0 1px rgba(109,179,253,0.8);
transition: box-shadow 250ms ease-out;
cursor: text;
}
}
}
.icon {
&.fliph::before {
position: relative;
left: 1px;
transform: scaleX(-1);
}
&.square {
display: inline-block;
width: 1.8em;
text-align: center;
}
}
.cotable {
display: flex;
flex-direction: column;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.cotable table {
table-layout: fixed;
width: 100%;
}
table + table {
display: block;
flex: 1;
overflow-y: auto;
}
table + table tbody {
width: 100%;
display: table;
}
table + table tr {
background: #FFF;
&:nth-child(odd):not(.selected):not(:active) {
background: #F5F5F5;
}
}
.rotate {
transform-origin: 50% 50%;
will-change: transform;
animation: rotate 500ms linear infinite;
}
@keyframes rotate {
0% { transform:rotate(0deg); }
100% { transform:rotate(360deg); }
}
================================================
FILE: webpack.config.babel.js
================================================
import pkg from './package.json';
import path from 'path';
import webpack from 'webpack';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import autoprefixer from 'autoprefixer';
const ENV = process.env.NODE_ENV || 'development';
module.exports = {
context: `${__dirname}/src`,
entry: './index.js',
target: 'web',
output: {
path: `${__dirname}/app/web`,
// publicPath: '/',
filename: 'bundle.js'
},
resolve: {
modulesDirectories: [
`${__dirname}/src/lib`,
`${__dirname}/node_modules`,
'node_modules'
]
// alias: {
// 'dropbox': path.resolve(__dirname, 'node_modules/dropbox/lib/dropbox.js')
// }
},
externals: [{
'remote': 'commonjs remote'
}],
module: {
noParse: [
/\b(node_modules|~)\/dropbox\//,
],
preLoaders: [
{
test: /\.(jsx?|css|less)$/,
exclude: /(src|webpack\-dev\-server|socket\.io\-client)/,
loader: 'source-map'
}
],
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|\/~\/|webpack\-dev\-server|socket\.io\-client)/,
loader: 'babel'
},
{
test: /\.(css|less)$/,
loader: ExtractTextPlugin.extract(
'style?sourceMap',
'css?sourceMap!postcss!less?sourceMap'
)
},
{
test: /\.(svg|woff2?|ttf|eot)$/,
loader: 'url'
}
]
},
postcss: () => [
autoprefixer({ browsers: 'last 2 versions' })
],
plugins: [
new webpack.NoErrorsPlugin(),
new ExtractTextPlugin('style.css', {
allChunks: true,
disable: ENV!=='production'
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
API_KEY: JSON.stringify(process.env.DROPBOX_API_KEY),
'process.env.NODE_ENV': JSON.stringify(ENV)
}),
new HtmlWebpackPlugin({
title: pkg.productName || pkg.name,
filename: 'index.html',
minify: { collapseWhitespace: true }
})
],
// turn off node shims
node: {
console: false,
global: false,
process: false,
Buffer: false,
__filename: false,
__dirname: false,
setImmediate: false
},
stats: { colors: true },
// Create Sourcemaps for the bundle
devtool: ENV==='development' ? 'inline-source-map' : 'source-map',
devServer: {
port: process.env.PORT || 19998,
contentBase: './build',
inline: true,
hot: true,
historyApiFallback: true
}
};
gitextract_bfsdldih/ ├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── electron/ │ ├── backend.js │ ├── index.js │ ├── menu.js │ └── package.json ├── package.json ├── src/ │ ├── assets/ │ │ ├── icon.icns │ │ └── oauth_receiver.html │ ├── components/ │ │ ├── app.js │ │ ├── file-list.js │ │ ├── path-bar.js │ │ └── sidebar.js │ ├── index.js │ ├── lib/ │ │ ├── dropbox-client.js │ │ ├── menu.js │ │ └── remote-require.js │ └── styles/ │ └── index.less └── webpack.config.babel.js
SYMBOL INDEX (49 symbols across 11 files)
FILE: electron/backend.js
function upload (line 4) | function upload(path, url, callback) {
FILE: electron/index.js
constant HOST (line 6) | const HOST = `localhost:${process.env.PORT || 19998}`;
constant DEV (line 8) | const DEV = process.env.NODE_ENV==='development';
function createMainWindow (line 23) | function createMainWindow() {
function overrideWindowOpen (line 54) | function overrideWindowOpen() {
FILE: electron/menu.js
constant MAC (line 5) | const MAC = process.platform==='darwin';
constant DEV (line 6) | const DEV = process.env.NODE_ENV==='dev';
constant MENU (line 7) | const MENU = [];
method click (line 69) | click(item, focusedWindow) {
method click (line 77) | click(item, focusedWindow) {
method click (line 86) | click(item, focusedWindow) {
FILE: src/components/app.js
method componentDidMount (line 19) | componentDidMount() {
method navigate (line 26) | @bind
method search (line 52) | @bind
method handleSearch (line 60) | @bind
method handleFile (line 70) | @bind
method openFile (line 81) | openFile(file) {
method to (line 95) | @bind
method getActions (line 100) | getActions() {
method onDragOver (line 107) | onDragOver(e) {
method onDrop (line 114) | @bind
method showFilesMenu (line 131) | @bind
method newFile (line 141) | newFile(name, path=this.state.path) {
method newDirectory (line 151) | newDirectory(name, path=this.state.path) {
method spawnContextMenu (line 161) | @bind
method no (line 175) | no() {
FILE: src/components/file-list.js
class FileList (line 8) | class FileList extends Component {
method handleKey (line 9) | @bind
method openSelected (line 27) | openSelected() {
method move (line 35) | move(delta) {
method componentDidMount (line 47) | componentDidMount() {
method componentWillUnmount (line 51) | componentWillUnmount() {
method componentDidUpdate (line 55) | componentDidUpdate() {
method render (line 60) | render({ files, action, ...props }, { selected }) {
function fileProxy (line 105) | function fileProxy(file, fn) {
FILE: src/components/sidebar.js
class Sidebar (line 4) | class Sidebar extends Component {
method render (line 17) | render({ path, actions }) {
FILE: src/index.js
function init (line 5) | function init() {
FILE: src/lib/dropbox-client.js
constant CLEANUPS (line 15) | const CLEANUPS = {};
function init (line 29) | function init(callback=NOOP) {
function stream (line 47) | function stream(path, callback) {
function open (line 66) | function open(path, options, callback) {
function watchAndUpload (line 84) | function watchAndUpload(localPath, path, onUpload) {
function upload (line 98) | function upload(localPath, path, callback) {
FILE: src/lib/menu.js
function createMenu (line 5) | function createMenu(template) {
function showMenu (line 10) | function showMenu(menu, x, y) {
function createDomMenu (line 14) | function createDomMenu(element) {
FILE: src/lib/remote-require.js
function remoteRequire (line 4) | function remoteRequire(module) {
FILE: webpack.config.babel.js
constant ENV (line 8) | const ENV = process.env.NODE_ENV || 'development';
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (36K chars).
[
{
"path": ".babelrc",
"chars": 211,
"preview": "{\n \"sourceMaps\": true,\n \"presets\": [\n \"es2015-minimal\",\n \"stage-0\"\n ],\n \"plugins\": [\n \"add-module-exports\","
},
{
"path": ".editorconfig",
"chars": 231,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".gitignore",
"chars": 60,
"preview": "node_modules\n/npm-debug.log\n/dist\n/build\n/app\n/design\n/.env\n"
},
{
"path": "LICENSE",
"chars": 1120,
"preview": "The MIT License (MIT)\n\nCopyright (c) Jason Miller <jason@developit.ca> (http://jasonformat.com)\n\nPermission is hereby gr"
},
{
"path": "README.md",
"chars": 1521,
"preview": "# Dropfox\n\nA dropbox client powered by [Preact], [Electron] and [Photon].\n\n> ### [Download Dropfox ➞](https://github.com"
},
{
"path": "electron/backend.js",
"chars": 353,
"preview": "import fs from 'fs';\nimport request from 'request';\n\nexport function upload(path, url, callback) {\n\tfunction done(err) {"
},
{
"path": "electron/index.js",
"chars": 1917,
"preview": "import { parse as parseUrl } from 'url';\nimport app from 'app';\nimport BrowserWindow from 'browser-window';\nimport menu "
},
{
"path": "electron/menu.js",
"chars": 2882,
"preview": "import { openExternal } from 'shell';\nimport app from 'app';\nimport Menu from 'menu';\n\nconst MAC = process.platform==='d"
},
{
"path": "electron/package.json",
"chars": 463,
"preview": "{\n \"name\": \"Dropfox\",\n \"productName\": \"Dropfox\",\n \"version\": \"1.2.0\",\n \"description\": \"Dropbox client, powered by Pr"
},
{
"path": "package.json",
"chars": 3339,
"preview": "{\n \"name\": \"dropfox\",\n \"productName\": \"Dropfox\",\n \"version\": \"1.2.0\",\n \"description\": \"Dropbox client, powered by Pr"
},
{
"path": "src/assets/oauth_receiver.html",
"chars": 489,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<script>\n\t\t\twindow.addEventListener('load', function() {\n\t\t\t\tvar opener = win"
},
{
"path": "src/components/app.js",
"chars": 5883,
"preview": "import { bind, memoize } from 'decko';\nimport { parallel } from 'praline';\nimport { Component, h, render } from 'preact'"
},
{
"path": "src/components/file-list.js",
"chars": 2733,
"preview": "import { bind, memoize } from 'decko';\nimport { h, Component } from 'preact';\nimport neatime from 'neatime';\nimport { Ic"
},
{
"path": "src/components/path-bar.js",
"chars": 434,
"preview": "import { h, Component } from 'preact';\nimport { ButtonGroup, Button } from 'preact-photon';\n\nconst EXISTS = x => x;\n\nexp"
},
{
"path": "src/components/sidebar.js",
"chars": 917,
"preview": "import { h, Component } from 'preact';\nimport { NavGroup } from 'preact-photon';\n\nexport default class Sidebar extends C"
},
{
"path": "src/index.js",
"chars": 283,
"preview": "import { h, render } from 'preact';\nimport './styles/index.less';\n\nlet root;\nfunction init() {\n\tlet App = require('./com"
},
{
"path": "src/lib/dropbox-client.js",
"chars": 2714,
"preview": "import Emitter from 'wildemitter';\nimport { debounce } from 'decko';\nimport { Client, AuthDriver } from 'dropbox';\nimpor"
},
{
"path": "src/lib/menu.js",
"chars": 898,
"preview": "import remote from 'remote';\nconst Menu = remote.require('menu');\nconst MenuItem = remote.require('menu-item');\n\nexport "
},
{
"path": "src/lib/remote-require.js",
"chars": 278,
"preview": "import remote from 'remote';\n\n// remote.require(), patched to work in both dev and prod mode:\nexport default function re"
},
{
"path": "src/styles/index.less",
"chars": 1995,
"preview": "@import (less) '~photon/dist/css/photon.css';\n\n#app {\n\t.toolbar {\n\t\t-webkit-app-region: drag;\n\t\t.form-control {\n\t\t\t-webk"
},
{
"path": "webpack.config.babel.js",
"chars": 2359,
"preview": "import pkg from './package.json';\nimport path from 'path';\nimport webpack from 'webpack';\nimport ExtractTextPlugin from "
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the developit/dropfox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (30.4 KB), approximately 9.7k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.