├── .gitignore ├── LICENSE ├── README.md ├── audio-player ├── .gitignore ├── README.md ├── esdoc.json ├── gulpfile.js ├── package.json ├── src │ ├── design.html │ ├── icons │ │ ├── add.svg │ │ ├── icons.idraw │ │ ├── next.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── prev.svg │ │ ├── remove.svg │ │ ├── speaker.svg │ │ ├── template.css │ │ ├── template.html │ │ └── template.styl │ ├── index.html │ ├── js │ │ ├── AppContext.js │ │ ├── actions │ │ │ ├── AudioPlayerAction.js │ │ │ └── MusicListAction.js │ │ ├── app.js │ │ ├── model │ │ │ ├── AudioPlayer.js │ │ │ ├── FileDialog.js │ │ │ ├── IndexedDBWrapper.js │ │ │ ├── MusicList.js │ │ │ └── Util.js │ │ ├── stores │ │ │ ├── AudioPlayerStore.js │ │ │ └── MusicListStore.js │ │ └── vm │ │ │ ├── DesignViewModel.js │ │ │ ├── MainViewModel.js │ │ │ ├── MusicListViewModel.js │ │ │ └── ToolbarViewModel.js │ ├── package.json │ └── stylus │ │ ├── App.styl │ │ ├── CommonColor.styl │ │ ├── MusicList.styl │ │ └── Toolbar.styl ├── ss.png └── test │ └── Util.test.js ├── simple-filer-without-browserify ├── .gitignore ├── README.md ├── gulpfile.js ├── package.json └── src │ ├── bower.json │ ├── css │ ├── icomoon.css │ └── style.css │ ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff │ ├── index.html │ ├── js │ ├── file-utility.js │ ├── jsx │ │ ├── explorer.jsx │ │ ├── folder-detail.jsx │ │ └── folder-tree.jsx │ └── main.js │ └── package.json └── simple-filer ├── .gitignore ├── README.md ├── gulpfile.js ├── package.json ├── src ├── bower.json ├── css │ ├── icomoon.css │ └── style.css ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── index.html ├── js │ ├── explorer.jsx │ ├── file-utility.js │ ├── folder-detail.jsx │ ├── folder-tree.jsx │ └── main.js └── package.json └── ss.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | dist/ 28 | release/ 29 | cache/ 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | *.map 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 akabeko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # examples-nw 2 | 3 | Examples of [nw.js](https://github.com/nwjs/nw.js "nw.js"). 4 | 5 | * [audio-player](https://github.com/akabekobeko/examples-nw/tree/master/audio-player "audio-player") 6 | * [simple-filer](https://github.com/akabekobeko/examples-nw/tree/master/simple-filer "simple-filer") 7 | * [simple-filer-without-browserify](https://github.com/akabekobeko/examples-nw/tree/master/simple-filer-without-browserify "simple-filer-without-browserify") -------------------------------------------------------------------------------- /audio-player/.gitignore: -------------------------------------------------------------------------------- 1 | esdoc/ 2 | release/ 3 | src/fonts/ 4 | 5 | bundle.* 6 | Icon.styl 7 | icon.css 8 | icon-sample.html 9 | -------------------------------------------------------------------------------- /audio-player/README.md: -------------------------------------------------------------------------------- 1 | # nw.js - Audio Player 2 | 3 | Example of simple audio player in [NW.js](https://github.com/nwjs/nw.js). 4 | 5 | ![Screenshot](ss.png) 6 | 7 | ## Installation & Build 8 | 9 | 1. Install [Node.js](https://nodejs.org/) 10 | 1. `git clone https://github.com/akabekobeko/examples-nw.git` 11 | 1. `cd audio player` 12 | 1. `npm install` 13 | 1. Run npm commands 14 | * `npm start` Development build & start watch files 15 | * `npm test` Unit tests 16 | * `npm run app` Launcher app on NW.js 17 | * `npm run esdoc` Create code documents 18 | * `npm run release` Release build ( create NW.js app package & code documents ) 19 | 20 | ## Remarks 21 | 22 | * [Using MP3 & MP4 (H.264) using the video & audio tags.](https://github.com/nwjs/nw.js/wiki/Using-MP3-%26-MP4-%28H.264%29-using-the--video--%26--audio--tags.) 23 | 24 | ## License 25 | 26 | MIT -------------------------------------------------------------------------------- /audio-player/esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src/js", 3 | "destination": "./esdoc", 4 | "test": { 5 | "type": "mocha", 6 | "source": "./test" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /audio-player/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require( 'gulp' ); 2 | var $ = require( 'gulp-load-plugins' )(); 3 | 4 | // 共通タスク設定 5 | var common = { 6 | src: './src', 7 | dest: './release', 8 | test: './test', 9 | isWatchify: false, 10 | isUglify: false 11 | }; 12 | 13 | // browserify タスクのファイル監視モード実行フラグを有効化 14 | gulp.task( 'browserify-watchfy', function( done ) { common.isWatchify = true; done(); } ); 15 | 16 | // browserify タスクの圧縮・最適化モード実行フラグを有効化 17 | gulp.task( 'browserify-uglify', function( done ) { common.isUglify = true; done(); } ); 18 | 19 | // JavaScript 間の依存解決とコンパイルを実行し、その結果を単一のファイルとして出力する 20 | gulp.task( 'browserify', $.watchify( function( watchify ) { 21 | var buffer = require( 'vinyl-buffer' ); 22 | var formatter = require( 'pretty-hrtime' ); 23 | var time = process.hrtime(); 24 | 25 | return gulp.src( [ common.src + '/js/App.js' ] ) 26 | .pipe( $.plumber() ) 27 | .pipe( watchify( { 28 | watch: common.isWatchify, 29 | basedir: './', 30 | debug: true, 31 | transform: [ 'babelify' ] 32 | } ) ) 33 | .pipe( buffer() ) 34 | .pipe( $.sourcemaps.init( { loadMaps: true } ) ) 35 | .pipe( $.if( common.isUglify, $.uglify() ) ) 36 | .pipe( $.rename( 'bundle.js' ) ) 37 | .pipe( $.sourcemaps.write( './' ) ) 38 | .pipe( gulp.dest( common.src ) ) 39 | .on( 'end', function() { 40 | var taskTime = formatter( process.hrtime( time ) ); 41 | $.util.log( 'Bundled', $.util.colors.green( 'bundle.js' ), 'in', $.util.colors.magenta( taskTime ) ); 42 | } ); 43 | } ) ); 44 | 45 | // Stylus コンパイルと結合 46 | gulp.task( 'stylus', function() { 47 | return gulp.src( [ common.src + '/stylus/App.styl' ] ) 48 | .pipe( $.plumber() ) 49 | .pipe( $.sourcemaps.init() ) 50 | .pipe( $.stylus() ) 51 | .pipe( $.rename( 'bundle.css' ) ) 52 | .pipe( $.minifyCss() ) 53 | .pipe( $.sourcemaps.write( '.' ) ) 54 | .pipe( gulp.dest( common.src ) ); 55 | } ); 56 | 57 | // アイコン フォント生成 58 | gulp.task( 'iconfont', function() { 59 | return gulp.src( common.src + '/icons/*.svg' ) 60 | .pipe( $.iconfont( { fontName: 'icon' } ) ) 61 | .on( 'codepoints', function( codepoints ) { 62 | var options = { 63 | className: 'icon', 64 | fontName: 'icon', 65 | fontPath: 'fonts/', 66 | cssFile: 'icon.css', 67 | glyphs: codepoints 68 | }; 69 | 70 | // Stylus 71 | gulp.src( common.src + '/icons/template.styl' ) 72 | .pipe( $.consolidate( 'lodash', options ) ) 73 | .pipe( $.rename( { basename: 'Icon' } ) ) 74 | .pipe( gulp.dest( common.src + '/stylus' ) ); 75 | 76 | // CSS ( 出力見本用 ) 77 | gulp.src( common.src + '/icons/template.css' ) 78 | .pipe( $.consolidate( 'lodash', options ) ) 79 | .pipe( $.rename( { basename: 'icon' } ) ) 80 | .pipe( gulp.dest( common.src ) ); 81 | 82 | // フォント出力見本 HTML ( CSS も必要 ) 83 | gulp.src( common.src + '/icons/template.html' ) 84 | .pipe( $.consolidate( 'lodash', options ) ) 85 | .pipe( $.rename( { basename: 'icon-sample' } ) ) 86 | .pipe( gulp.dest( common.src ) ); 87 | } ) 88 | .pipe( gulp.dest( common.src + '/fonts' ) ); 89 | } ); 90 | 91 | // アイコンフォントも含めた Stylus コンパイルと結合 92 | // これらは依存関係があるものの、watch の兼ね合いでタスクとしては依存設定したくない 93 | gulp.task( 'stylus-with-iconfont', [ 'iconfont' ], function( done ) { 94 | var runSequence = require( 'run-sequence' ); 95 | runSequence( 'stylus' ); 96 | done(); 97 | } ); 98 | 99 | // リリース用イメージ削除 100 | gulp.task( 'clean', function( done ) { 101 | var del = require( 'del' ); 102 | del( [ common.dest + '/src' ], done ); 103 | } ); 104 | 105 | // リリース用イメージの生成とコピー 106 | gulp.task( 'release-build', [ 'clean', 'browserify-uglify', 'browserify', 'stylus-with-iconfont' ], function() { 107 | return gulp.src( 108 | [ 109 | common.src + '/package.json', 110 | common.src + '/index.html', 111 | common.src + '/bundle.js', 112 | common.src + '/bundle.css', 113 | common.src + '/fonts/**' 114 | ], 115 | { base: common.src } 116 | ) 117 | .pipe( gulp.dest( common.dest + '/src' ) ); 118 | } ); 119 | 120 | // nw.js ビルド 121 | gulp.task( 'nw', [ 'release-build' ], function() { 122 | var builder = new ( require( 'node-webkit-builder' ) )( { 123 | version: '0.12.2', 124 | files: [ common.dest + '/src/**' ], 125 | buildDir: common.dest + '/bin', 126 | cacheDir: common.dest + '/nw', 127 | platforms: [ 'osx64' ] 128 | } ); 129 | 130 | builder.on( 'log', function( message ) { 131 | $.util.log( 'node-webkit-builder', message ); 132 | } ); 133 | 134 | return builder.build().catch( function( err ) { 135 | $.util.log( 'node-webkit-builder', err ); 136 | } ); 137 | } ); 138 | 139 | // ファイル監視 140 | gulp.task( 'watch', [ 'stylus-with-iconfont', 'browserify-watchfy', 'browserify' ], function () { 141 | gulp.watch( [ common.src + '/stylus/*.styl' ], [ 'stylus' ] ); 142 | gulp.watch( [ common.src + '/icons/*.svg' ], [ 'iconfont' ] ); 143 | } ); 144 | 145 | gulp.task( 'test', function() { 146 | gulp.src( [ common.test + '/*.js' ] ); 147 | } ); 148 | 149 | // 既定タスク 150 | gulp.task( 'default', [ 'watch' ] ); 151 | -------------------------------------------------------------------------------- /audio-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-player", 3 | "version": "1.0.3", 4 | "description": "Example of the audio player in nw.js.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "gulp watch", 8 | "test": "mocha --compilers js:espower-babel/guess test/**/*.js", 9 | "app": "nw ./src", 10 | "esdoc": "esdoc -c esdoc.json", 11 | "css": "gulp stylus", 12 | "release": "npm run esdoc && gulp nw" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/akabekobeko/examples-nw" 17 | }, 18 | "keywords": [ 19 | "NW.js", 20 | "Audio", 21 | "React", 22 | "Flux" 23 | ], 24 | "author": "akabeko", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babelify": "^6.1.1", 28 | "browserify": "^9.0.8", 29 | "del": "^1.1.1", 30 | "esdoc": "^0.1.2", 31 | "espower-babel": "^3.2.0", 32 | "gulp": "^3.8.11", 33 | "gulp-concat": "^2.5.2", 34 | "gulp-consolidate": "^0.1.2", 35 | "gulp-iconfont": "^1.0.0", 36 | "gulp-if": "^1.2.5", 37 | "gulp-load-plugins": "^0.10.0", 38 | "gulp-minify-css": "^1.1.1", 39 | "gulp-plumber": "^1.0.0", 40 | "gulp-rename": "^1.2.2", 41 | "gulp-sourcemaps": "^1.5.2", 42 | "gulp-stylus": "^2.0.1", 43 | "gulp-uglify": "^1.2.0", 44 | "gulp-useref": "^1.1.2", 45 | "gulp-util": "^3.0.4", 46 | "gulp-watchify": "^0.5.0", 47 | "lodash": "^3.8.0", 48 | "mocha": "^2.2.5", 49 | "node-webkit-builder": "^1.0.11", 50 | "nw": "^0.12.2", 51 | "power-assert": "^0.11.0", 52 | "pretty-hrtime": "^1.0.0", 53 | "run-sequence": "^1.1.0", 54 | "vinyl-buffer": "^1.0.0", 55 | "watchify": "^3.2.1" 56 | }, 57 | "dependencies": { 58 | "material-flux": "^1.2.1", 59 | "musicmetadata": "^1.0.1", 60 | "object-assign": "^2.0.0", 61 | "react": "^0.13.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /audio-player/src/design.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Audio Player 6 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /audio-player/src/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /audio-player/src/icons/icons.idraw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/audio-player/src/icons/icons.idraw -------------------------------------------------------------------------------- /audio-player/src/icons/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /audio-player/src/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /audio-player/src/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /audio-player/src/icons/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /audio-player/src/icons/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /audio-player/src/icons/speaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /audio-player/src/icons/template.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "<%= fontName %>"; 3 | src: url( "<%= fontPath %><%= fontName %>.eot" ); 4 | src: url( "<%= fontPath %><%= fontName %>.eot?#iefix" ) format( "eot" ), 5 | url( "<%= fontPath %><%= fontName %>.woff" ) format( "woff" ), 6 | url( "<%= fontPath %><%= fontName %>.ttf" ) format( "truetype" ), 7 | url( "<%= fontPath %><%= fontName %>.svg#<%= fontName %>" ) format( "svg" ); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="<%= className %>-"], [class*=" <%= className %>-"] { 13 | font-family: "<%= fontName %>"; 14 | font-style: normal; 15 | font-weight: normal; 16 | font-variant: normal; 17 | text-transform: none; 18 | line-height: 1; 19 | speak: none; 20 | 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | <% _.each( glyphs, function( glyph ) { %>.<%= className %>-<%= glyph.name %>:before { content: "\<%= glyph.codepoint.toString( 16 ).toUpperCase() %>" } 26 | <% }); %> -------------------------------------------------------------------------------- /audio-player/src/icons/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= fontName %> 6 | 7 | 50 | 51 | 52 |

<%= fontName %>

53 | 54 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /audio-player/src/icons/template.styl: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family "<%= fontName %>" 3 | src url( "<%= fontPath %><%= fontName %>.eot" ) 4 | src url( "<%= fontPath %><%= fontName %>.eot?#iefix" ) format( "eot" ), 5 | url( "<%= fontPath %><%= fontName %>.woff" ) format( "woff" ), 6 | url( "<%= fontPath %><%= fontName %>.ttf" ) format( "truetype" ), 7 | url( "<%= fontPath %><%= fontName %>.svg#<%= fontName %>" ) format( "svg" ) 8 | font-weight normal 9 | font-style normal 10 | } 11 | 12 | [class^="<%= className %>-"], [class*=" <%= className %>-"] { 13 | font-family "<%= fontName %>" 14 | font-style normal 15 | font-weight normal 16 | font-variant normal 17 | text-transform none 18 | line-height 1 19 | speak none 20 | 21 | -webkit-font-smoothing antialiased 22 | -moz-osx-font-smoothing grayscale 23 | } 24 | 25 | <% _.each( glyphs, function( glyph ) { %>.<%= className %>-<%= glyph.name %>:before { 26 | content "\<%= glyph.codepoint.toString( 16 ).toUpperCase() %>" 27 | } 28 | <% }) %> -------------------------------------------------------------------------------- /audio-player/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Audio Player 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /audio-player/src/js/AppContext.js: -------------------------------------------------------------------------------- 1 | import { Context } from 'material-flux'; 2 | import MusicListAction from './actions/MusicListAction.js'; 3 | import MusicListStore from './stores/MusicListStore.js'; 4 | import AudioPlayerAction from './actions/AudioPlayerAction.js'; 5 | import AudioPlayerStore from './stores/AudioPlayerStore.js'; 6 | 7 | /** 8 | * アプリケーションを表します。 9 | */ 10 | export default class AppContext extends Context { 11 | /** 12 | * インスタンスを初期化します。 13 | */ 14 | constructor() { 15 | super(); 16 | 17 | /** 18 | * 音楽情報の操作。 19 | * @type {MusicListAction} 20 | */ 21 | this.musicListAction = new MusicListAction( this ); 22 | 23 | /** 24 | * 音楽情報の管理。 25 | * @type {MusicListStore} 26 | */ 27 | this.musicListStore = new MusicListStore( this ); 28 | 29 | /** 30 | * 音声操作 31 | * @type {AudioPlayerAction} 32 | */ 33 | this.audioPlayerAction = new AudioPlayerAction( this ); 34 | 35 | /** 36 | * 音声データ管理。 37 | * @type {AudioPlayerStore} 38 | */ 39 | this.audioPlayerStore = new AudioPlayerStore( this, this.musicListStore ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /audio-player/src/js/actions/AudioPlayerAction.js: -------------------------------------------------------------------------------- 1 | import { Action } from 'material-flux'; 2 | 3 | /** 4 | * 音声プレーヤーの操作種別を定義します。 5 | * @type {Object} 6 | */ 7 | export const Keys = { 8 | play: Symbol( 'AudioPlayerAction.play' ), 9 | pause: Symbol( 'AudioPlayerAction.pause' ), 10 | stop: Symbol( 'AudioPlayerAction.stop' ), 11 | seek: Symbol( 'AudioPlayerAction.seek' ), 12 | volume: Symbol( 'AudioPlayerAction.volume' ), 13 | unselect: Symbol( 'AudioPlayerAction.unselect' ) 14 | }; 15 | 16 | /** 17 | * 音声プレーヤーを操作します。 18 | */ 19 | export default class AudioPlayerAction extends Action { 20 | /** 21 | * 音声を再生します。 22 | * 23 | * @param {Music} music 再生対象とする音楽情報。 24 | */ 25 | play( music ) { 26 | this.dispatch( Keys.play, music ); 27 | } 28 | 29 | /** 30 | * 音声再生を一時停止します。 31 | */ 32 | pause() { 33 | this.dispatch( Keys.pause ); 34 | } 35 | 36 | /** 37 | * 音声再生を停止します。 38 | */ 39 | stop() { 40 | this.dispatch( Keys.stop ); 41 | } 42 | 43 | /** 44 | * 再生位置を変更します。 45 | * 46 | * @param {Number} playbackTime 新しい再生位置 ( 秒単位 )。 47 | */ 48 | seek( playbackTime ) { 49 | this.dispatch( Keys.seek, playbackTime ); 50 | } 51 | 52 | /** 53 | * 音量を変更します。 54 | * 55 | * @param {Number} volume 新しい音量 ( 0 〜 100 )。 56 | */ 57 | volume( volume ) { 58 | this.dispatch( Keys.volume, volume ); 59 | } 60 | 61 | /** 62 | * 再生対象としている曲の選択を解除します。 63 | */ 64 | unselect() { 65 | this.dispatch( Keys.unselect ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /audio-player/src/js/actions/MusicListAction.js: -------------------------------------------------------------------------------- 1 | import { Action } from 'material-flux'; 2 | 3 | /** 4 | * 音楽リストの操作種別を定義します。 5 | * @type {Object} 6 | */ 7 | export const Keys = { 8 | init: Symbol( 'MusicListAction.init' ), 9 | select: Symbol( 'MusicListAction.select' ), 10 | add: Symbol( 'MusicListAction.add' ), 11 | remove: Symbol( 'MusicListAction.remove' ), 12 | clear: Symbol( 'MusicListAction.clear' ) 13 | }; 14 | 15 | /** 16 | * 音楽リストを操作します。 17 | */ 18 | export default class MusicListAction extends Action { 19 | 20 | /** 21 | * 音楽リストを初期化します。 22 | */ 23 | init() { 24 | this.dispatch( Keys.init ); 25 | } 26 | 27 | /** 28 | * 音楽を追加します。 29 | * 30 | * @param {Music} music 音楽情報。 31 | */ 32 | select( music ) { 33 | this.dispatch( Keys.select, music ); 34 | } 35 | 36 | /** 37 | * 音楽を追加します。 38 | */ 39 | add() { 40 | this.dispatch( Keys.add ); 41 | } 42 | 43 | /** 44 | * 音楽を削除します。 45 | * 46 | * @param {Number} musicId 削除対象とする音楽の識別子。 47 | */ 48 | remove( musicId ) { 49 | this.dispatch( Keys.remove, musicId ); 50 | } 51 | 52 | /** 53 | * すべての音楽を消去します。 54 | */ 55 | clear() { 56 | this.dispatch( Keys.clear ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /audio-player/src/js/app.js: -------------------------------------------------------------------------------- 1 | import AppContext from './AppContext.js'; 2 | import { SetupDesignViewModel } from './vm/DesignViewModel.js'; 3 | import { SetupMainViewModel } from './vm/MainViewModel.js'; 4 | 5 | /** 6 | * アプリケーション。 7 | * @type {AppContext} 8 | */ 9 | let context = null; 10 | 11 | /** 12 | * アプリケーションのエントリー ポイントです。 13 | */ 14 | window.onload = () => { 15 | context = new AppContext(); 16 | 17 | const selector = 'body'; 18 | if( window.testDesignMode ) { 19 | SetupDesignViewModel( selector ); 20 | } else { 21 | SetupMainViewModel( context, selector ); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /audio-player/src/js/model/AudioPlayer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 音声プレーヤーを提供します。 3 | * 4 | * @see referred: http://github.com/eipark/buffaudio 5 | * 6 | * @throws {Error} Web Audio API が未定義です。 7 | */ 8 | export default class AudioPlayer { 9 | /** 10 | * インスタンスを初期化します。 11 | */ 12 | constructor() { 13 | /** 14 | * 音声操作コンテキスト。 15 | * @type {AudioContext|webkitAudioContext} 16 | */ 17 | this._audioContext = ( () => { 18 | const audioContext = ( window.AudioContext || window.webkitAudioContext ); 19 | if( audioContext ) { return new audioContext(); } 20 | 21 | throw new Error( 'Web Audio API is not supported.' ); 22 | } )(); 23 | 24 | /** 25 | * 音量調整ノード。 26 | * @type {GainNode} 27 | */ 28 | this._gainNode = this._audioContext.createGain(); 29 | this._gainNode.gain.value = 1.0; 30 | this._gainNode.connect( this._audioContext.destination ); 31 | 32 | /** 33 | * 音声解析ノード。 34 | * @type {AnalyserNode} 35 | */ 36 | this._analyserNode = this._audioContext.createAnalyser(); 37 | this._analyserNode.fftSize = 128; 38 | this._analyserNode.connect( this._gainNode ); 39 | 40 | /** 41 | * 音声ソース ノード。 42 | * @type {AudioBufferSourceNode} 43 | */ 44 | this._sourceNode = null; 45 | 46 | /** 47 | * 音声バッファ。 48 | * @type {AudioBuffer} 49 | */ 50 | this._audioBuffer = null; 51 | 52 | /** 53 | * 音声が再生中であることを示す値。 54 | * @type {Boolean} 55 | */ 56 | this._isPlaying = false; 57 | 58 | /** 59 | * 再生位置 ( 秒単位 )。 60 | * @type {Number} 61 | */ 62 | this._playbackTime = 0; 63 | 64 | /** 65 | * 再生を開始した時の日時 ( ミリ秒単位 )。 66 | * AudioBufferSourceNode の再生操作は start/stop のみをサポートし、pause が存在しません。 67 | * よって、これを実装するために start 時間を記録し、pause された時の現時刻から引いて再開位置を算出します。 68 | * 69 | * @type {Number} 70 | */ 71 | this._startTimestamp = 0; 72 | } 73 | 74 | /** 75 | * 音声データを再生対象として開きます。 76 | * 77 | * @param {ArrayBuffer} buffer 音声データ。 78 | * @param {Function} callback 処理が完了したときに呼び出される関数。 79 | */ 80 | open( buffer, callback ) { 81 | this._audioContext.decodeAudioData( buffer, 82 | ( audioBuffer ) => { 83 | this.close(); 84 | this._audioBuffer = audioBuffer; 85 | this._initSourceNode(); 86 | callback(); 87 | }, 88 | () => { 89 | // webkitAudioContext だとエラーが取れないので自前指定 90 | callback( new Error( 'Faild to decode for audio data.' ) ); 91 | } 92 | ); 93 | } 94 | 95 | /** 96 | * 音楽ファイルのパス情報から音楽プレーヤーを生成します。 97 | * 98 | * @param {String} filePath 音楽ファイルのパス情報。 99 | * @param {Function} callback 処理が完了したときに呼び出される関数。 100 | * 101 | * @throws {Error} filePath または callback が未定義です。 102 | */ 103 | openFromFile( filePath, callback ) { 104 | if( !( filePath && callback ) ) { throw new Error( 'Arguments is not defined.' ); } 105 | 106 | const fs = window.require( 'fs' ); 107 | fs.readFile( filePath, ( err, data ) => { 108 | if( err ) { 109 | callback( err ); 110 | } else { 111 | this.open( this._toArrayBuffer( data ), callback ); 112 | } 113 | } ); 114 | } 115 | 116 | /** 117 | * 音声データの URL から音楽プレーヤーを生成します。 118 | * 119 | * @param {String} url 音声データの URL。 120 | * @param {Function} callback 処理が完了したときに呼び出される関数。 121 | * 122 | * @throws {Error} filePath または callback が未定義です。 123 | */ 124 | openFromURL( url, callback ) { 125 | if( !( url && callback ) ) { throw new Error( 'Arguments is not defined.' ); } 126 | 127 | const request = new XMLHttpRequest(); 128 | request.open( 'GET', url ); 129 | request.responseType = 'arraybuffer'; 130 | 131 | request.onload = () => { 132 | this.open( request.response, callback ); 133 | }; 134 | 135 | request.onerror = ( err ) => { 136 | callback( err ); 137 | }; 138 | } 139 | 140 | /** 141 | * 再生対象としている音声データを閉じます。 142 | */ 143 | close() { 144 | this.stop(); 145 | this._audioBuffer = null; 146 | this._playbackTime = 0; 147 | this._startTimestamp = 0; 148 | } 149 | 150 | /** 151 | * 音声の再生を開始します。 152 | * 153 | * @return {Boolean} 成功時は true。 154 | */ 155 | play() { 156 | console.log( '[play]' ); 157 | if( this._isPlaying ) { return false; } 158 | 159 | this._initSourceNode(); 160 | this._sourceNode.start( 0, this._playbackTime ); 161 | this._startTimestamp = Date.now(); 162 | this._isPlaying = true; 163 | 164 | return true; 165 | } 166 | 167 | /** 168 | * 音声の再生を一時停止します。 169 | * 170 | * @return {Boolean} 成功時は true。 171 | */ 172 | pause() { 173 | console.log( '[pause]' ); 174 | if( !( this._isPlaying ) ) { return false; } 175 | 176 | this.stop( true ); 177 | return true; 178 | } 179 | 180 | /** 181 | * 音声の再生を停止します。 182 | * 183 | * @param {Boolean} pause 一時停止する場合は true。それ以外は停止。 184 | */ 185 | stop( pause ) { 186 | console.log( '[stop]' ); 187 | if( pause ) { 188 | // 一時停止呼ならば onended を無効化しておく 189 | // この処理がないと play の後に onended が遅延実行され、再生状態がおかしくなる 190 | // 191 | if( this._sourceNode ) { 192 | this._sourceNode.onended = null; 193 | this._sourceNode.stop(); 194 | this._sourceNode = null; 195 | } 196 | 197 | // 次回の再生時に復元するための位置を記録 198 | this._playbackTime = this.playbackTime(); 199 | 200 | } else { 201 | if( this._sourceNode ) { 202 | this._sourceNode.stop(); 203 | this._sourceNode = null; 204 | } 205 | 206 | this._playbackTime = 0; 207 | } 208 | 209 | // this.playbackTime() 内で現在位置を算出してからフラグを無効化する 210 | this._isPlaying = false; 211 | } 212 | 213 | /** 214 | * 音声の再生位置を変更します。 215 | * 216 | * @param {Number} playbackTime 再生位置 ( 秒単位 )。 217 | * 218 | * @return {Boolean} 成功時は true。 219 | */ 220 | seek( playbackTime ) { 221 | console.log( '[seek]' ); 222 | if( playbackTime === undefined ) { return false; } 223 | 224 | // 時間指定が文字列になる現象を避けるため、ここで強制的に数値化しておく 225 | playbackTime = Number( playbackTime ); 226 | if( this.duration() < playbackTime ) { 227 | console.log( '[ERROR] Seek time is greater than duration of audio buffer.' ); 228 | return false; 229 | } 230 | 231 | if( this._isPlaying ) { 232 | this.pause(); 233 | this._playbackTime = playbackTime; 234 | this.play(); 235 | } else { 236 | this._playbackTime = playbackTime; 237 | } 238 | 239 | return true; 240 | } 241 | 242 | /** 243 | * 演奏時間を取得します。 244 | * 245 | * @return {Number} 演奏時間 ( 秒単位 )。 246 | */ 247 | duration() { 248 | return ( this._audioBuffer ? Math.round( this._audioBuffer.duration ) : 0 ); 249 | } 250 | 251 | /** 252 | * 再生位置を取得します。 253 | * 254 | * @return {Number} 再生位置 ( 秒単位 )。 255 | */ 256 | playbackTime() { 257 | if( this._isPlaying ) { 258 | return ( Math.round( ( Date.now() - this._startTimestamp ) / 1000 ) + this._playbackTime ); 259 | } else { 260 | return this._playbackTime; 261 | } 262 | } 263 | 264 | /** 265 | * 音声の周波数スペクトルを取得します。 266 | * 267 | * @return {Array} スペクトル。 268 | */ 269 | spectrums() { 270 | const spectrums = new Uint8Array( this._analyserNode.frequencyBinCount ); 271 | this._analyserNode.getByteFrequencyData( spectrums ); 272 | 273 | return spectrums; 274 | } 275 | 276 | /** 277 | * 音量を取得します。 278 | * 279 | * @return {Number} 音量。範囲は 0 〜 100 となります。 280 | */ 281 | volume() { 282 | return ( this._gainNode.gain.value * 100 ); 283 | } 284 | 285 | /** 286 | * 音量を設定します。 287 | * 288 | * @param {Number} value 音量。範囲は 0 〜 100 となります。 289 | */ 290 | setVolume( value ) { 291 | if( 0 <= value && value <= 100 ) { 292 | this._gainNode.gain.value = ( value / 100 ); 293 | } 294 | } 295 | 296 | /** 297 | * 音声再生が終了した時に発生します。 298 | */ 299 | _onEnded() { 300 | console.log( '[onend]' ); 301 | } 302 | 303 | /** 304 | * 音声ソース ノードを初期化します。 305 | */ 306 | _initSourceNode() { 307 | this._sourceNode = this._audioContext.createBufferSource(); 308 | this._sourceNode.buffer = this._audioBuffer; 309 | this._sourceNode.connect( this._analyserNode ); 310 | 311 | const onEnded = this._onEnded.bind( this ); 312 | this._sourceNode.onended = onEnded; 313 | } 314 | 315 | /** 316 | * Node.js の Buffer を JavaScript の ArrayBuffer に変換します。 317 | * 318 | * @see referred: http://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer 319 | * 320 | * @param {Buffer} Node.js の Buffer。 321 | * 322 | * @return {ArrayBuffer} JavaScript の ArrayBuffer。 323 | */ 324 | _toArrayBuffer( buffer ) { 325 | const ab = new ArrayBuffer( buffer.length ); 326 | const view = new Uint8Array( ab ); 327 | 328 | for( let i = 0, max = buffer.length; i < max; ++i ) { 329 | view[ i ] = buffer[ i ]; 330 | } 331 | 332 | return ab; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /audio-player/src/js/model/FileDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ファイル選択ダイアログを提供します。 3 | * 4 | */ 5 | export class OpenFileDialog { 6 | /** 7 | * インスタンスを初期化します。 8 | * 9 | * @param {String} accept 種別。 10 | * @param {Boolean} multiple 複数選択するなら true。 11 | * @param {Function} callback ダイアログが閉じられる時に呼び出される関数。 12 | */ 13 | constructor( accept, multiple, callback ) { 14 | /** 15 | * input 要素。 16 | * @type {Element} 17 | */ 18 | this._element = document.createElement( 'input' ); 19 | this._element.setAttribute( 'type', 'file' ); 20 | this._element.addEventListener( 'change', ( ev ) => { 21 | if( this._callback ) { 22 | this._callback( ev.target.file || ev.target.files ); 23 | } 24 | } ); 25 | 26 | /** 27 | * ダイアログが閉じられる時に呼び出される関数。 28 | * @type {Function} 29 | */ 30 | this._callback = callback; 31 | 32 | this.setAccept( accept ); 33 | this.setMultipe( multiple ); 34 | } 35 | 36 | /** 37 | * ダイアログを表示します。 38 | * 39 | * @param {Function} callback ダイアログが閉じられる時に呼び出される関数。 40 | */ 41 | show( callback ) { 42 | if( callback ) { 43 | this._callback = callback; 44 | } 45 | 46 | this._element.click(); 47 | } 48 | 49 | /** 50 | * ダイアログで開くファイル種別を設定します。 51 | * 52 | * @param {String} accept 種別。 53 | */ 54 | setAccept( accept ) { 55 | this._element.setAttribute( 'accept', accept ); 56 | } 57 | 58 | /** 59 | * 複数ファイルを選択するための値を設定します。 60 | * 61 | * @param {Boolean} multiple 複数選択するなら true。 62 | */ 63 | setMultipe( multiple ) { 64 | if( multiple ) { 65 | this._element.setAttribute( 'multiple', true ); 66 | this._element.setAttribute( 'name', 'files[]' ); 67 | } else { 68 | this._element.removeAttribute( 'multiple' ); 69 | this._element.setAttribute( 'name', 'file' ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /audio-player/src/js/model/IndexedDBWrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IndexedDB を操作しやすくするためのユーティリティです。 3 | */ 4 | export default class IndexedDBWrapper { 5 | /** 6 | * インスタンスを初期化します。 7 | * 8 | * @param {String} dbName データベース名。 9 | * @param {Number} dbVersion データベースのバージョン番号。 10 | * @param {String} dbStoreName ストア名。 11 | */ 12 | constructor( dbName, dbVersion, dbStoreName ) { 13 | // IndexedDB チェック 14 | this._indexedDB = ( window.indexedDB || window.mozIndexedDB || window.msIndexedDB || window.webkitIndexedDB ); 15 | if( !( this._indexedDB ) ) { 16 | throw new Error( 'IndexedDB not supported.' ); 17 | } 18 | 19 | /** 20 | * データベース。 21 | * @type {Object} 22 | */ 23 | this._db = null; 24 | 25 | /** 26 | * データベース名。 27 | * @type {String} 28 | */ 29 | this._dbName = dbName; 30 | 31 | /** 32 | * データベースのバージョン番号。 33 | * @type {Number} 34 | */ 35 | this._dbVersion = dbVersion; 36 | 37 | /** 38 | * ストア名。 39 | * @type {[String]} 40 | */ 41 | this._dbStoreName = dbStoreName; 42 | } 43 | 44 | /** 45 | * 各関数でコールバックが未指定だった時、代りに実行される関数です。 46 | * 47 | * @param {Error} err エラー情報。 48 | * 49 | * @return 常に false。カーソル系で継続可否を問い合わせるコールバックの場合、中断となる。 50 | */ 51 | _defaultCallback( err ) { 52 | if( err ) { 53 | console.log( 'DB [callback]: Error, ' + err.message ); 54 | } else { 55 | console.log( 'DB [callback]: Success' ); 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * データベースを開きます。 63 | * 64 | * @param {Object} params パラメータ。create = createObjectStore オプション( 必須 )。 65 | * @param {Function} callback 処理が終了した時に呼び出される関数。 66 | * 67 | * @throws {Error} params.create が未指定です。 68 | */ 69 | open( params, callback ) { 70 | if( !( params && params.create ) ) { throw new Error( 'Invalid arguments' ); } 71 | 72 | const onFinish = ( callback || this._defaultCallback ); 73 | const request = this._indexedDB.open( this._dbName, this._dbVersion ); 74 | 75 | request.onupgradeneeded = ( ev ) => { 76 | // ストア生成 77 | this._db = ev.target.result; 78 | const store = this._db.createObjectStore( this._dbStoreName, params.create ); 79 | 80 | // インデックス 81 | if( params.index && 0 < params.index.length ) { 82 | params.index.forEach( function( index ) { 83 | store.createIndex( index.name, index.keyPath, index.params ); 84 | } ); 85 | } 86 | 87 | ev.target.transaction.oncomplete = () => { 88 | onFinish(); 89 | }; 90 | }; 91 | 92 | request.onsuccess = ( ev ) => { 93 | this._db = ev.target.result; 94 | onFinish(); 95 | }; 96 | 97 | request.onerror = ( ev ) => { 98 | onFinish( ev.target.error ); 99 | }; 100 | } 101 | 102 | /** 103 | * データベースを破棄します。 104 | * 105 | * @param {Function} callback 処理が終了した時に呼び出される関数。 106 | */ 107 | dispose( callback ) { 108 | const onFinish = ( callback || this._defaultCallback ); 109 | 110 | if( this._db ) { 111 | this._db.close(); 112 | this._db = null; 113 | } 114 | 115 | const request = this._indexedDB.deleteDatabase( this._dbName ); 116 | request.onsuccess = () => { 117 | onFinish(); 118 | }; 119 | 120 | request.onerror = ( ev ) => { 121 | console.log( 'DB [ dispose ]: Error, ' + ev.target.error ); 122 | onFinish( ev.target.error ); 123 | }; 124 | } 125 | 126 | /** 127 | * データを全て消去します。 128 | * 129 | * @param {Function} callback 処理が終了した時に呼び出される関数。 130 | */ 131 | clear( callback ) { 132 | if( !( this._db ) ) { return; } 133 | 134 | const onFinish = ( callback || this._defaultCallback ); 135 | const transaction = this._db.transaction( this._dbStoreName, 'readwrite' ); 136 | const store = transaction.objectStore( this._dbStoreName ); 137 | const request = store.clear(); 138 | 139 | request.onsuccess = () => { 140 | onFinish(); 141 | }; 142 | 143 | request.onerror = ( ev ) => { 144 | onFinish( ev.target.error ); 145 | }; 146 | } 147 | 148 | /** 149 | * 全アイテムを読み取ります。 150 | * 151 | * @param {Function} callback 処理が終了した時に呼び出される関数。 152 | */ 153 | readAll( callback ) { 154 | if( !( this._db ) ) { return; } 155 | 156 | const onFinish = ( callback || this._defaultCallback ); 157 | const transaction = this._db.transaction( this._dbStoreName, 'readonly' ); 158 | const store = transaction.objectStore( this._dbStoreName ); 159 | const request = store.openCursor(); 160 | const items = []; 161 | 162 | request.onsuccess = ( ev ) => { 163 | const cursor = ev.target.result; 164 | if( cursor ) { 165 | items.push( cursor.value ); 166 | cursor.continue(); 167 | 168 | } else { 169 | onFinish( null, items ); 170 | } 171 | }; 172 | 173 | request.onerror = ( ev ) => { 174 | onFinish( ev.target.error ); 175 | }; 176 | } 177 | 178 | /** 179 | * 全アイテムを中断されるまで読み取ります。 180 | * 181 | * @param {Function} callback アイテムが 1 件、読み込まれるごとに呼び出される関数。true を返すと次の値を読み取ります。 182 | */ 183 | readSome( callback ) { 184 | if( !( this._db ) ) { return; } 185 | 186 | const onFinish = ( callback || this._defaultCallback ); 187 | const transaction = this._db.transaction( this._dbStoreName, 'readonly' ); 188 | const store = transaction.objectStore( this._dbStoreName ); 189 | const request = store.openCursor(); 190 | 191 | request.onsuccess = ( ev ) => { 192 | const cursor = ev.target.result; 193 | if( cursor ) { 194 | if( onFinish( null, cursor.value ) ) { 195 | cursor.continue(); 196 | } 197 | } else { 198 | onFinish( null, cursor.value ); 199 | } 200 | }; 201 | 202 | request.onerror = ( ev ) => { 203 | onFinish( ev.target.error ); 204 | }; 205 | } 206 | 207 | /** 208 | * アイテムを追加または更新します。 209 | * 210 | * @param {Object} item アイテム。id プロパティが有効値 ( 1 以上の整数 ) なら既存アイテムを更新します。 211 | * @param {Function} callback 処理が終了した時に呼び出される関数。 212 | */ 213 | add( item, callback ) { 214 | if( !( this._db ) ) { return; } 215 | 216 | const onFinish = ( callback || this._defaultCallback ); 217 | const transaction = this._db.transaction( this._dbStoreName, 'readwrite' ); 218 | const store = transaction.objectStore( this._dbStoreName ); 219 | const request = store.put( item ); 220 | 221 | request.onsuccess = ( ev ) => { 222 | item.id = ev.target.result; 223 | onFinish( null, item ); 224 | }; 225 | 226 | request.onerror = ( ev ) => { 227 | onFinish( ev.target.error, item ); 228 | }; 229 | } 230 | 231 | /** 232 | * アイテムを削除します。 233 | * 234 | * @param {Number} id 音楽情報の識別子。 235 | * @param {Function} callback 処理が終了した時に呼び出される関数。 236 | */ 237 | remove( id, callback ) { 238 | if( !( this._db ) ) { return; } 239 | 240 | const onFinish = ( callback || this._defaultCallback ); 241 | const transaction = this._db.transaction( this._dbStoreName, 'readwrite' ); 242 | const store = transaction.objectStore( this._dbStoreName ); 243 | const request = store.delete( id ); 244 | 245 | request.onsuccess = () => { 246 | onFinish( null, id ); 247 | }; 248 | 249 | request.onerror = ( ev ) => { 250 | onFinish( ev.target.error, id ); 251 | }; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /audio-player/src/js/model/MusicList.js: -------------------------------------------------------------------------------- 1 | import IndexedDBWrapper from './IndexedDBWrapper.js'; 2 | 3 | /** 4 | * 音楽情報リストを管理します。 5 | */ 6 | export default class MusicList { 7 | /** 8 | * インスタンスを初期化します。 9 | */ 10 | constructor() { 11 | /** 12 | * オーディオ要素。 13 | * @type {Element} 14 | */ 15 | this._audioElement = document.createElement( 'audio' ); 16 | 17 | /** 18 | * データベース。 19 | * @type {PouchDB} 20 | */ 21 | this._db = new IndexedDBWrapper( 'music_db', 1, 'musics' ); 22 | } 23 | 24 | /** 25 | * データベースを初期化します。 26 | * 27 | * @param {Function} callback 初期化が完了した時に呼び出される関数。 28 | */ 29 | init( callback ) { 30 | const params = { 31 | create: { 32 | keyPath: 'id', 33 | autoIncrement: true 34 | }, 35 | index: [ 36 | { name: 'path', keyPath: 'path', params: { unique: true } } 37 | ] 38 | }; 39 | 40 | this._db.open( params, ( err ) => { 41 | callback( err ); 42 | } ); 43 | } 44 | 45 | /** 46 | * 音声ファイルを追加します。 47 | * 48 | * @param {File} file ファイル情報。input[type="file"] で読み取った情報を指定してください。 49 | * @param {Function} callback 処理が完了した時に呼び出される関数。 50 | */ 51 | add( file, callback ) { 52 | this._readMetadata( file, ( err, music ) => { 53 | if( err ) { 54 | callback( err ); 55 | } else { 56 | this._db.add( music, callback ); 57 | } 58 | } ); 59 | } 60 | 61 | /** 62 | * 音楽情報を削除します。 63 | * 64 | * @param {Number} musicId 削除対象となる音楽情報の識別子。 65 | * @param {Function} callback コールバック関数。 66 | */ 67 | remove( musicId, callback ) { 68 | this._db.remove( musicId, callback ); 69 | } 70 | 71 | /** 72 | * 全ての曲を読み取ります。 73 | * 74 | * @param {Function} callback 処理が完了した時に呼び出される関数。 75 | */ 76 | readAll( callback ) { 77 | this._db.readAll( callback ); 78 | } 79 | 80 | /** 81 | * 音声ファイルのメタデータを読み取ります。 82 | * 83 | * @param {File} file ファイル情報。input[type="file"] で読み取った情報を指定してください。 84 | * @param {Function} callback 処理が完了した時に呼び出される関数。 85 | */ 86 | _readMetadata( file, callback ) { 87 | // MIME チェック 88 | if( !( this._audioElement.canPlayType( file.type ) ) ) { 89 | callback( new Error( 'Unsupported type "' + file.type + '".' ) ); 90 | return; 91 | } 92 | 93 | const mm = window.require( 'musicmetadata' ); 94 | const fs = window.require( 'fs' ); 95 | const stream = fs.createReadStream( file.path ); 96 | 97 | mm( stream, { duration: true }, ( err, metadata ) => { 98 | if( err ) { 99 | callback( err ); 100 | } else { 101 | callback( null, { 102 | type: file.type, 103 | path: file.path, 104 | title: metadata.title || '', 105 | artist: ( 0 < metadata.artist.length ? metadata.artist[ 0 ] : '' ), 106 | album: metadata.album || '', 107 | duration: metadata.duration 108 | } ); 109 | } 110 | } ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /audio-player/src/js/model/Util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ユーティリティ関数を提供します。 3 | * 4 | * @type {Object} 5 | */ 6 | export class Util { 7 | /** 8 | * 秒単位の演奏時間を文字列化します。 9 | * 10 | * @param {Number} duration 演奏時間 ( 秒単位 )。 11 | * 12 | * @return {String} 文字列化された演奏時間。 13 | */ 14 | secondsToString( duration ) { 15 | const h = ( duration / 3600 | 0 ); 16 | const m = ( ( duration % 3600 ) / 60 | 0 ); 17 | const s = ( duration % 60 ); 18 | 19 | function padding( num ) { 20 | return ( '0' + num ).slice( -2 ); 21 | } 22 | 23 | return ( 24 | 0 < h ? h + ':' + padding( m ) + ':' + padding( s ) : 25 | 0 < m ? m + ':' + padding( s ) : 26 | '0:' + padding( s ) 27 | ); 28 | } 29 | } 30 | 31 | // シングルトンとして公開 32 | export default new Util(); 33 | -------------------------------------------------------------------------------- /audio-player/src/js/stores/AudioPlayerStore.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'material-flux'; 2 | import { Keys } from '../actions/AudioPlayerAction.js'; 3 | import AudioPlayer from '../model/AudioPlayer.js'; 4 | 5 | /** 6 | * 音声プレーヤーの再生状態を定義します。 7 | * @type {Object} 8 | */ 9 | export const PlayState = { 10 | /** 停止されている。 */ 11 | Stopped: 0, 12 | 13 | /** 再生中。 */ 14 | Playing: 1, 15 | 16 | /** 一時停止されている。 */ 17 | Paused: 2 18 | }; 19 | 20 | /** 21 | * 音声プレーヤーを操作します。 22 | * 23 | * @type {AudioPlayerStore} 24 | */ 25 | export default class AudioPlayerStore extends Store { 26 | /** 27 | * インスタンスを初期化します。 28 | * 29 | * @param {Context} context コンテキスト。 30 | * @param {MusicListStore} musicListStore 音楽リスト。 31 | */ 32 | constructor( context, musicListStore ) { 33 | super( context ); 34 | 35 | this.register( Keys.play, this._actionPlay ); 36 | this.register( Keys.pause, this._actionPause ); 37 | this.register( Keys.stop, this._actionStop ); 38 | this.register( Keys.seek, this._actionSeek ); 39 | this.register( Keys.volume, this._actionVolume ); 40 | this.register( Keys.unselect, this._actionUnselect ); 41 | 42 | /** 43 | * 変更監視される値。 44 | * @type {Object} 45 | */ 46 | this.state = { 47 | /** 48 | * 再生対象となる音楽情報。 49 | * @type {Music} 50 | */ 51 | current: null, 52 | 53 | /** 54 | * 再生状態。 55 | * @type {PlayState} 56 | */ 57 | playState: PlayState.Stopped 58 | }; 59 | 60 | /** 61 | * 音声プレーヤー。 62 | * @type {AudioPlayer} 63 | */ 64 | this._audioPlayer = new AudioPlayer(); 65 | 66 | /** 67 | * 音楽リスト。 68 | * @type {MusicListStore} 69 | */ 70 | this._musicListStore = musicListStore; 71 | 72 | /** 73 | * 再生時間と演奏終了を監視するためのタイマー。 74 | * @type {Number} 75 | */ 76 | this._timer = null; 77 | } 78 | 79 | /** 80 | * 再生対象となる音楽情報を取得します。 81 | * 82 | * @return {Music} 音楽情報。 83 | */ 84 | get current() { 85 | return this.state.current; 86 | } 87 | 88 | /** 89 | * 再生状態を示す値を取得します。 90 | * 91 | * @return {PlayState} 再生状態。 92 | */ 93 | get playState() { 94 | return this.state.playState; 95 | } 96 | 97 | /** 98 | * 演奏時間を取得します。 99 | * 100 | * @return {Number} 演奏時間 ( 秒単位 )。 101 | */ 102 | get duration() { 103 | const d = this._audioPlayer.duration(); 104 | return ( d === 0 ? ( this.state.current ? this.state.current.duration : 0 ) : d ); 105 | } 106 | 107 | /** 108 | * 再生位置を取得します。 109 | * 110 | * @return {Number} 再生位置 ( 秒単位 )。 111 | */ 112 | get playbackTime() { 113 | return this._audioPlayer.playbackTime(); 114 | } 115 | 116 | /** 117 | * 音声の周波数スペクトルを取得します。 118 | * 119 | * @return {Array} スペクトル。 120 | */ 121 | get spectrums() { 122 | return this._audioPlayer.spectrums(); 123 | } 124 | 125 | /** 126 | * 音量を取得します。 127 | * 128 | * @return {Number} 音量。範囲は 0 〜 100 となります。 129 | */ 130 | get volume() { 131 | return this._audioPlayer.volume(); 132 | } 133 | 134 | /** 135 | * 再生を開始します。 136 | * 137 | * @param {Music} music 再生対象となる音楽情報。 138 | */ 139 | _actionPlay( music ) { 140 | if( music ) { 141 | this._audioPlayer.openFromFile( music.path, ( err ) => { 142 | if( err ) { 143 | console.log( err.message ); 144 | } else { 145 | const state = { current: music }; 146 | if( this._audioPlayer.play() ) { 147 | this._playTimer(); 148 | state.playState = PlayState.Playing; 149 | } 150 | 151 | this.setState( state ); 152 | } 153 | } ); 154 | 155 | } else if( this.state.playState !== PlayState.Playing && this._audioPlayer.play() ) { 156 | this._playTimer(); 157 | this.setState( { playState: PlayState.Playing } ); 158 | } 159 | } 160 | 161 | /** 162 | * 再生を一時停止します。 163 | */ 164 | _actionPause() { 165 | if( this.state.playState === PlayState.Playing && this._audioPlayer.pause() ) { 166 | this._playTimer( true ); 167 | this.setState( { playState: PlayState.Paused } ); 168 | } 169 | } 170 | 171 | /** 172 | * 再生を停止します。 173 | * 状態管理を簡素化するため、この操作は再生状態に関わらず常に強制実行されるようにしています。 174 | */ 175 | _actionStop() { 176 | this._playTimer( true ); 177 | this._audioPlayer.stop(); 178 | this.setState( { playState: PlayState.Stopped } ); 179 | } 180 | 181 | /** 182 | * 再生位置を変更します。 183 | * 184 | * @param {Number} playbackTime 新しい再生位置 ( 秒単位 )。 185 | * 186 | * @return {Boolean} 成功時は true。 187 | */ 188 | _actionSeek( playbackTime ) { 189 | if( this._audioPlayer.seek( playbackTime ) ) { 190 | this.setState(); 191 | } 192 | } 193 | 194 | /** 195 | * 音量を設定します。 196 | * 197 | * @param {Number} value 音量。範囲は 0 〜 100 となります。 198 | */ 199 | _actionVolume( value ) { 200 | this._audioPlayer.setVolume( value ); 201 | this.setState(); 202 | } 203 | 204 | /** 205 | * 再生対象としている曲の選択状態を解除します。 206 | */ 207 | _actionUnselect() { 208 | if( !( this.state.current ) ) { return; } 209 | 210 | if( this.state.playState !== PlayState.Stopped ) { 211 | this._actionStop(); 212 | } 213 | 214 | this.setState( { current: null } ); 215 | } 216 | 217 | /** 218 | * 再生時間と演奏終了を監視するためのタイマーを開始・停止します。 219 | * 220 | * @param {Boolean} isStop タイマーを停止させる場合は true。 221 | */ 222 | _playTimer( isStop ) { 223 | if( isStop ) { 224 | clearInterval( this._timer ); 225 | } else { 226 | this._timer = setInterval( () => { 227 | if( this._audioPlayer.duration() <= this._audioPlayer.playbackTime() ) { 228 | // 再生終了 229 | clearInterval( this._timer ); 230 | this._actionStop(); 231 | 232 | let music = this._musicListStore.next( this.state.current ); 233 | if( music ) { 234 | // 次の曲を再生 ( 更新は play 内で通知される ) 235 | this._actionPlay( music ); 236 | return; 237 | } 238 | } 239 | 240 | // 再生時間の更新を通知 241 | this.setState(); 242 | 243 | }, 1000 ); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /audio-player/src/js/stores/MusicListStore.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'material-flux'; 2 | import { Keys } from '../actions/MusicListAction.js'; 3 | import { OpenFileDialog } from '../model/FileDialog.js'; 4 | import MusicList from '../model/MusicList.js'; 5 | 6 | /** 7 | * 曲リストを操作します。 8 | * 9 | * @type {MusicListStore} 10 | */ 11 | export default class MusicListStore extends Store { 12 | /** 13 | * インスタンスを初期化します。 14 | * 15 | * @param {Context} context コンテキスト。 16 | */ 17 | constructor( context ) { 18 | super( context ); 19 | 20 | this.register( Keys.init, this._actionInit ); 21 | this.register( Keys.select, this._actionSelect ); 22 | this.register( Keys.add, this._actionAdd ); 23 | this.register( Keys.remove, this._actionRemove ); 24 | 25 | /** 26 | * 変更監視される値。 27 | * @type {Object} 28 | */ 29 | this.state = { 30 | /** 31 | * 唯一の曲リスト。 32 | * @type {Array.} 33 | */ 34 | musics: [], 35 | 36 | /** 37 | * リスト上で選択されている曲。 38 | * @type {Music} 39 | */ 40 | current: null 41 | }; 42 | 43 | /** 44 | * 唯一の曲リスト操作オブジェクト。 45 | * @type {MusicList} 46 | */ 47 | this._musicList = new MusicList(); 48 | 49 | /** 50 | * ファイル選択ダイアログ。 51 | * @type {OpenFileDialog} 52 | */ 53 | this._openFileDialog = new OpenFileDialog( 'audio/*', true, this._onSelectFiles.bind( this ) ); 54 | } 55 | 56 | /** 57 | * すべての曲を取得します。 58 | * 59 | * @return {Array.} 曲情報コレクション。 60 | */ 61 | get musics() { 62 | return this.state.musics; 63 | } 64 | 65 | /** 66 | * 現在選択されている曲を取得します。 67 | * 68 | * @return {Music} 曲情報。何も選択されていない場合は null。 69 | */ 70 | get current() { 71 | return this.state.current; 72 | } 73 | 74 | /** 75 | * 次の曲を取得します。 76 | * 77 | * music が未指定の場合、曲リストで選択されているものの前の曲を取得します。 78 | * 指定された曲、または選択されている曲がリストの末尾だった場合は null を返します。 79 | * 80 | * @param {Music} target 基準となる曲。 81 | * @param {Boolean} prev 前の曲を得る場合は true。既定は false。 82 | * 83 | * @return {Music} 成功時は曲情報。それ以外は null。 84 | */ 85 | next( target, prev ) { 86 | const current = ( target ? target : this.state.current ); 87 | if( !( current ) ) { return null; } 88 | 89 | let next = null; 90 | this.state.musics.some( ( music, index ) => { 91 | if( music.id === current.id ) { 92 | const position = ( prev ? index - 1 : index + 1 ); 93 | next = this.state.musics[ position ]; 94 | return true; 95 | } 96 | 97 | return false; 98 | } ); 99 | 100 | return next; 101 | } 102 | 103 | /** 104 | * 音楽リストを初期化します。 105 | */ 106 | _actionInit() { 107 | this._musicList.init( ( err ) => { 108 | if( err ) { 109 | console.error( err ); 110 | } else { 111 | this._musicList.readAll( ( err2, musics ) => { 112 | if( err2 ) { 113 | console.error( err2 ); 114 | } else { 115 | const state = { musics: musics }; 116 | if( 0 < musics.length ) { 117 | state.current = musics[ 0 ]; 118 | } 119 | 120 | this.setState( state ); 121 | } 122 | } ); 123 | } 124 | } ); 125 | } 126 | 127 | /** 128 | * 曲を選択します。 129 | * 130 | * @param {Music} target 選択対象となる曲。 131 | */ 132 | _actionSelect( target ) { 133 | if( this.state.current && target && this.state.current.id === target.id ) { return false; } 134 | 135 | //let err = new Error( 'Failed to select the music, not found.' ); 136 | let newMusic = null; 137 | this.state.musics.some( ( music ) => { 138 | if( target.id === music.id ) { 139 | newMusic = music; 140 | return true; 141 | } 142 | 143 | return false; 144 | } ); 145 | 146 | if( newMusic ) { 147 | this.setState( { current: newMusic } ); 148 | } else { 149 | console.error( 'Failed to select the music, not found.' ); 150 | } 151 | } 152 | 153 | /** 154 | * 音声ファイルを追加します。 155 | */ 156 | _actionAdd() { 157 | this._openFileDialog.show(); 158 | } 159 | 160 | /** 161 | * 曲を削除します。 162 | * 163 | * @param {Number} musicId 削除対象となる曲の識別子。 164 | */ 165 | _actionRemove( musicId ) { 166 | this._musicList.remove( musicId, ( err ) => { 167 | if( err ) { 168 | console.error( err ); 169 | } else { 170 | const newMusics = this.state.musics.filter( ( music ) => { 171 | return ( music.id !== musicId ); 172 | } ); 173 | 174 | if( newMusics.length !== this.state.musics.length ) { 175 | this.setState( { musics: newMusics } ); 176 | } else { 177 | console.error( 'Failed to remove the music, not found.' ); 178 | } 179 | } 180 | } ); 181 | } 182 | 183 | /** 184 | * 追加対象となるファイルが選択された時に発生します。 185 | * 186 | * @param {FileList} files ファイル情報コレクション。 187 | */ 188 | _onSelectFiles( files ) { 189 | if( !( files && 0 < files.length ) ) { return; } 190 | 191 | const onAdded = ( err, music ) => { 192 | if( err ) { 193 | console.error( err ); 194 | } else { 195 | const newMusics = this.state.musics.concat( music ); 196 | this.setState( { musics: newMusics } ); 197 | } 198 | }; 199 | 200 | // FileList は Array ではないため forEach を利用できない 201 | for( let i = 0, max = files.length; i < max; ++i ) { 202 | this._musicList.add( files[ i ], onAdded ); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /audio-player/src/js/vm/DesignViewModel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ObjectAssign from 'object-assign'; 3 | import { ToolbarView } from './ToolbarViewModel.js'; 4 | import { MusicListView } from './MusicListViewModel.js'; 5 | import { PlayState } from '../stores/AudioPlayerStore.js'; 6 | 7 | /** 8 | * アプリケーションのエントリー ポイントになるコンポーネント ( デザイン確認用 ) です。 9 | * 10 | * @type {ReactClass} 11 | */ 12 | export default class DesignViewModel extends React.Component { 13 | /** 14 | * コンポーネントを初期化します。 15 | * 16 | * @param {Object} props プロパティ。 17 | */ 18 | constructor( props ) { 19 | super( props ); 20 | 21 | const musics = [ 22 | { id: 1, title: 'test1', artist: 'artist1', album: 'album1', duration: 150 }, 23 | { id: 2, title: 'test2', artist: 'artist2', album: 'album2', duration: 120 } 24 | ]; 25 | 26 | /** 27 | * コンポーネントの状態。 28 | * @type {Object} 29 | */ 30 | this.state = { 31 | // 音楽リスト 32 | musics: musics, 33 | current: musics[ 0 ], 34 | onSelectMusic: this._onSelectMusic, 35 | onSelectPlay: this._onSelectPlay, 36 | 37 | // ツールバー 38 | currentPlay: musics[ 0 ], 39 | playState: PlayState.Stopped, 40 | duration: musics[ 0 ].duration, 41 | playbackTime: 0, 42 | volume: 100, 43 | onPressButton: this._onPressButton, 44 | onVolumeChange: this._onVolumeChange, 45 | onPositionChange: this._onPositionChange 46 | }; 47 | } 48 | 49 | /** 50 | * コンポーネントを描画します。 51 | * 52 | * @return {Object} React エレメント。 53 | */ 54 | render() { 55 | const comp = ObjectAssign( {}, this.state, { self: this } ); 56 | return ( 57 |
58 | { ToolbarView( comp ) } 59 | { MusicListView( comp ) } 60 |
61 | ); 62 | } 63 | 64 | /** 65 | * 音楽が選択された時に発生します。 66 | * 67 | * @param {Object} music 音楽。 68 | */ 69 | _onSelectMusic( music ) { 70 | console.log( '_onSelectMusic' ); 71 | this.setState( { current: music, currentPlay: music, duration: music.duration } ); 72 | } 73 | 74 | /** 75 | * 音楽が再生対象として選択された時に発生します。 76 | * 77 | * @param {Object} music 音楽。 78 | */ 79 | _onSelectPlay( /*music*/ ) { 80 | console.log( '_onSelectPlay' ); 81 | } 82 | 83 | /** 84 | * ボタンが押された時に発生します。 85 | * 86 | * @param {String} type ボタン種別。 87 | */ 88 | _onPressButton( type ) { 89 | console.log( '_onPressButton: type = ' + type ); 90 | } 91 | 92 | /** 93 | * 音量が変更された時に発生します。 94 | * 95 | * @param {Object} ev イベント情報。 96 | */ 97 | _onVolumeChange( ev ) { 98 | console.log( '_onVolumeChange: value = ' + ev.target.value ); 99 | this.setState( { volume: ev.target.value } ); 100 | } 101 | 102 | /** 103 | * 再生位置が変更された時に発生します。 104 | * 105 | * @param {Object} ev イベント情報。 106 | */ 107 | _onPositionChange( ev ) { 108 | console.log( '_onPositionChange: value = ' + ev.target.value ); 109 | this.setState( { playbackTime: ev.target.value } ); 110 | } 111 | } 112 | 113 | /** 114 | * デザイン確認用コンポーネントを設定します。 115 | * 116 | * @param {String} selector コンポーネントを配置する要素のセレクター。 117 | */ 118 | export function SetupDesignViewModel( selector ) { 119 | React.render( 120 | , 121 | document.querySelector( selector ) 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /audio-player/src/js/vm/MainViewModel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlayState } from '../stores/AudioPlayerStore.js'; 3 | import ToolbarViewModel from './ToolbarViewModel.js'; 4 | import MusicListViewModel from './MusicListViewModel.js'; 5 | 6 | /** 7 | * アプリケーションのメイン ウィンドウとなるコンポーネントです。 8 | * 9 | * @type {ReactClass} 10 | */ 11 | export default class MainViewModel extends React.Component { 12 | /** 13 | * コンポーネントを初期化します。 14 | * 15 | * @param {Object} props プロパティ。 16 | */ 17 | constructor( props ) { 18 | super( props ); 19 | 20 | /** 21 | * コンポーネントの状態。 22 | * @type {Object} 23 | */ 24 | this.state = { 25 | musics: [], 26 | current: null, 27 | currentPlay: null, 28 | playState: PlayState.Stopped, 29 | duration: 0, 30 | playbackTime: 0, 31 | volume: 100 32 | }; 33 | 34 | // Store の館対象となる bind 済み Listener を一意にするためのフィールド 35 | this.__onChangeAudioPlayer = this._onChangeAudioPlayer.bind( this ); 36 | this.__onChangeMusicList = this._onChangeMusicList.bind( this ); 37 | } 38 | 39 | /** 40 | * コンポーネントが配置される時に発生します。 41 | */ 42 | componentDidMount() { 43 | this.props.context.audioPlayerStore.onChange( this.__onChangeAudioPlayer ); 44 | this.props.context.musicListStore.onChange( this.__onChangeMusicList ); 45 | this.props.context.musicListAction.init(); 46 | } 47 | 48 | /** 49 | * コンポーネント配置が解除される時に発生します。 50 | */ 51 | componentWillUnmount() { 52 | this.props.context.audioPlayerStore.removeChangeListener( this.__onChangeAudioPlayer ); 53 | this.props.context.musicListStore.removeChangeListener( this.__onChangeMusicList ); 54 | } 55 | 56 | /** 57 | * コンポーネントを描画します。 58 | * 59 | * @return {Object} React エレメント。 60 | */ 61 | render() { 62 | return ( 63 |
64 | 73 | 79 | 80 |
81 | ); 82 | } 83 | 84 | /** 85 | * 音楽プレーヤーが更新された時に発生します。 86 | */ 87 | _onChangeAudioPlayer() { 88 | this.setState( { 89 | currentPlay: this.props.context.audioPlayerStore.current, 90 | playState: this.props.context.audioPlayerStore.playState, 91 | duration: this.props.context.audioPlayerStore.duration, 92 | playbackTime: this.props.context.audioPlayerStore.playbackTime, 93 | volume: this.props.context.audioPlayerStore.volume 94 | } ); 95 | } 96 | 97 | /** 98 | * 音楽リストが更新された時に発生します。 99 | */ 100 | _onChangeMusicList() { 101 | if( this.state.playState === PlayState.Stopped ) { 102 | this.setState( { 103 | musics: this.props.context.musicListStore.musics, 104 | current: this.props.context.musicListStore.current, 105 | currentPlay: this.props.context.audioPlayerStore.current 106 | } ); 107 | } else { 108 | this.setState( { 109 | musics: this.props.context.musicListStore.musics, 110 | current: this.props.context.musicListStore.current 111 | } ); 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * メイン ウィンドウ用コンポーネントを設定します。 118 | * 119 | * @param {AppContext} context コンテキスト。 120 | * @param {String} selector コンポーネントを配置する要素のセレクター。 121 | */ 122 | export function SetupMainViewModel( context, selector ) { 123 | React.render( 124 | , 125 | document.querySelector( selector ) 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /audio-player/src/js/vm/MusicListViewModel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ObjectAssign from 'object-assign'; 3 | import Util from '../model/Util.js'; 4 | import { PlayState } from '../stores/AudioPlayerStore.js'; 5 | 6 | /** 7 | * 音楽リストのアイテムを描画します。 8 | * 9 | * @param {Object} comp コンポーネント。 10 | * @param {Numbet} index リスト上のインデックス。 11 | * @param {Music} music 音楽情報。 12 | * @param {Boolean} selected 曲がリスト上で選択されているなら true。 13 | * @param {Boolean} playing 曲が再生中なら true。 14 | * 15 | * @return {ReactElement} React エレメント。 16 | */ 17 | function renderItem( comp, index, music, selected, playing ) { 18 | return ( 19 | 24 | 25 | { ( playing ? ( ) : null ) } 26 | 27 | { index + 1 } 28 | { music.title } 29 | { music.artist } 30 | { music.album } 31 | { Util.secondsToString( music.duration ) } 32 | 33 | ); 34 | } 35 | 36 | /** 37 | * 音楽リスト用コンポーネントを描画します。 38 | * 39 | * @param {Object} comp コンポーネント。 40 | * 41 | * @return {ReactElement} React エレメント。 42 | */ 43 | export function MusicListView( comp ) { 44 | const items = comp.musics.map( ( music, index ) => { 45 | const selected = ( comp.current && comp.current.id === music.id ); 46 | const playing = ( comp.playing && comp.currentPlay && comp.currentPlay.id === music.id ); 47 | return renderItem( comp, index, music, selected, playing ); 48 | } ); 49 | 50 | const style = { width: '1em' }; 51 | return ( 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | { items } 66 | 67 |
#TitleArtisAlbumDuration
68 |
69 | ); 70 | } 71 | 72 | /** 73 | * 音楽リストの Model - View を仲介するコンポーネントです。 74 | * 75 | * @type {ReactClass} 76 | */ 77 | export default class MusicListViewModel extends React.Component { 78 | /** 79 | * コンポーネントを初期化します。 80 | * 81 | * @param {Object} props プロパティ。 82 | */ 83 | constructor( props ) { 84 | super( props ); 85 | } 86 | 87 | /** 88 | * コンポーネントを描画します。 89 | * 90 | * @return {Object} React エレメント。 91 | */ 92 | render() { 93 | return MusicListView( ObjectAssign( {}, this.props, { 94 | self: this, 95 | playing: ( this.props.playState !== PlayState.Stopped ), 96 | onSelectMusic: this._onSelectMusic, 97 | onSelectPlay: this._onSelectPlay 98 | } ) ); 99 | } 100 | 101 | /** 102 | * 音楽が選択された時に発生します。 103 | * 104 | * @param {Object} music 音楽。 105 | */ 106 | _onSelectMusic( music ) { 107 | this.props.context.musicListAction.select( music ); 108 | } 109 | 110 | /** 111 | * 音楽が再生対象として選択された時に発生します。 112 | * 113 | * @param {Object} music 音楽。 114 | */ 115 | _onSelectPlay( music ) { 116 | this.props.context.audioPlayerAction.play( music ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /audio-player/src/js/vm/ToolbarViewModel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ObjectAssign from 'object-assign'; 3 | import Util from '../model/Util.js'; 4 | import { PlayState } from '../stores/AudioPlayerStore.js'; 5 | 6 | /** 7 | * ボタンを描画します。 8 | * 9 | * @param {Object} comp コンポーネント。 10 | * @param {String} type ボタン種別。 11 | * 12 | * @return {ReactElement} React エレメント。 13 | */ 14 | function renderButton( comp, type ) { 15 | return ( 16 |
17 | 18 |
19 | ); 20 | } 21 | 22 | /** 23 | * ツールバー用コンポーネントを描画します。 24 | * 25 | * @param {Object} comp コンポーネント。 26 | * 27 | * @return {ReactElement} React エレメント。 28 | */ 29 | export function ToolbarView( comp ) { 30 | const music = comp.currentPlay; 31 | const title = ( music ? music.title : '--' ); 32 | const duration = ( comp.duration === 0 ? ( music ? music.duration : 0 ) : comp.duration ); 33 | const playpause = ( comp.playState === PlayState.Playing ? 'pause' : 'play' ); 34 | 35 | return ( 36 |
37 |
38 |
39 | { renderButton( comp, 'prev' ) } 40 | { renderButton( comp, playpause ) } 41 | { renderButton( comp, 'next' ) } 42 | 49 |
50 |
51 |
52 |
53 | { Util.secondsToString( comp.playbackTime ) } 54 |
55 |
56 | { title } 57 |
58 |
59 | { Util.secondsToString( duration ) } 60 |
61 |
62 | 69 |
70 |
71 |
72 | { renderButton( comp, 'remove' ) } 73 | { renderButton( comp, 'add' ) } 74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | 81 | /** 82 | * ツールバー用コンポーネントです。 83 | * 84 | * @type {ReactClass} 85 | */ 86 | export default class ToolbarViewModel extends React.Component { 87 | /** 88 | * コンポーネントを初期化します。 89 | * 90 | * @param {Object} props プロパティ。 91 | */ 92 | constructor( props ) { 93 | super( props ); 94 | } 95 | 96 | /** 97 | * コンポーネントを描画します。 98 | * 99 | * @return {Object} React エレメント。 100 | */ 101 | render() { 102 | return ToolbarView( ObjectAssign( {}, this.props, { 103 | self: this, 104 | onPressButton: this._onPressButton, 105 | onVolumeChange: this._onVolumeChange, 106 | onPositionChange: this._onPositionChange 107 | } ) ); 108 | } 109 | 110 | /** 111 | * ボタンが押された時に発生します。 112 | * 113 | * @param {String} type ボタン種別。 114 | */ 115 | _onPressButton( type ) { 116 | switch( type ) { 117 | case 'play': 118 | this._play(); 119 | break; 120 | 121 | case 'pause': 122 | this.props.context.audioPlayerAction.pause(); 123 | break; 124 | 125 | case 'prev': 126 | this._moveNext( true ); 127 | break; 128 | 129 | case 'next': 130 | this._moveNext(); 131 | break; 132 | 133 | case 'add': 134 | this.props.context.musicListAction.add(); 135 | break; 136 | 137 | case 'remove': 138 | this._remove(); 139 | break; 140 | } 141 | } 142 | 143 | /** 144 | * 音量が変更された時に発生します。 145 | * 146 | * @param {Object} ev イベント情報。 147 | */ 148 | _onVolumeChange( ev ) { 149 | this.props.context.audioPlayerAction.volume( ev.target.value ); 150 | } 151 | 152 | /** 153 | * 再生位置が変更された時に発生します。 154 | * 155 | * @param {Object} ev イベント情報。 156 | */ 157 | _onPositionChange( ev ) { 158 | this.props.context.audioPlayerAction.seek( ev.target.value ); 159 | } 160 | 161 | /** 162 | * 曲を再生します。 163 | */ 164 | _play() { 165 | if( this.props.playState === PlayState.Stopped ) { 166 | this.props.context.audioPlayerAction.play( this.props.currentPlay ); 167 | } else { 168 | this.props.context.audioPlayerAction.play(); 169 | } 170 | } 171 | 172 | /** 173 | * 曲選択を変更します。 174 | * 175 | * @param {Boolan} prev 前の曲を選ぶなら true。 176 | */ 177 | _moveNext( prev ) { 178 | let music = this.props.context.musicListStore.next( this.props.currentPlay, prev ); 179 | if( !( music ) ) { return; } 180 | 181 | if( this.props.playState === PlayState.Stopped ) { 182 | this.props.context.musicListAction.select( music ); 183 | } else { 184 | this.props.context.audioPlayerAction.play( music ); 185 | } 186 | } 187 | 188 | /** 189 | * 選択している曲を削除します。 190 | */ 191 | _remove() { 192 | // リスト上の曲を対象とする 193 | const current = this.props.current; 194 | if( !( current ) ) { return; } 195 | 196 | const currentPlay = this.props.currentPlay; 197 | if( currentPlay && currentPlay.id === current.id ) { 198 | if( this.props.playState === PlayState.Stopped ) { 199 | this.props.context.musicListAction.remove( current.id ); 200 | } else { 201 | console.error( 'Failed to remove the music, is playing.' ); 202 | } 203 | 204 | } else { 205 | this.props.context.musicListAction.remove( current.id ); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /audio-player/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AudioPlayer", 3 | "version": "1.0.3", 4 | "description": "Example of the audio player in nw.js.", 5 | "main": "index.html", 6 | "window": { 7 | "title": "Audio Player", 8 | "toolbar": true, 9 | "frame": true, 10 | "resizable": true, 11 | "width": 1024, 12 | "height": 640, 13 | "min_width": 800, 14 | "min_height": 480, 15 | "position": "mouse" 16 | }, 17 | "platformOverrides": { 18 | "osx": { 19 | "window": { 20 | "toolbar": false 21 | } 22 | }, 23 | "win": { 24 | "window": { 25 | "toolbar": false 26 | } 27 | }, 28 | "linux": { 29 | "window": { 30 | "toolbar": false 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /audio-player/src/stylus/App.styl: -------------------------------------------------------------------------------- 1 | @import "CommonColor.styl" 2 | 3 | // ページ全体 4 | body, html { 5 | width 100% 6 | height 100% 7 | margin 0 8 | padding 0 9 | } 10 | 11 | // アプリケーション部分 12 | .app { 13 | position relative 14 | width 100% 15 | height 100% 16 | cursor default 17 | font-family sans-serif 18 | color color_adule 19 | } 20 | 21 | // UI 上のテキストを選択不可とするための指定 22 | .unselectable { 23 | -webkit-touch-callout none 24 | -webkit-user-select none 25 | -khtml-user-select none 26 | -moz-user-select none 27 | -ms-user-select none 28 | user-select none 29 | } 30 | 31 | @import "Icon.styl" 32 | @import "Toolbar.styl" 33 | @import "MusicList.styl" 34 | -------------------------------------------------------------------------------- /audio-player/src/stylus/CommonColor.styl: -------------------------------------------------------------------------------- 1 | color_gray_light_ex = #f8f8f8 2 | color_gray_light = #ecf0f1 3 | color_gray = #bdc3c7 4 | color_gray_dark = #95a5a6 5 | 6 | color_adule = #34495e 7 | 8 | color_blue_light = #9fd6fe 9 | 10 | -------------------------------------------------------------------------------- /audio-player/src/stylus/MusicList.styl: -------------------------------------------------------------------------------- 1 | @import "CommonColor.styl" 2 | 3 | // 音楽リスト領域 4 | .music-list { 5 | position absolute 6 | left 0 7 | right 0 8 | top 3em 9 | bottom 0 10 | overflow auto 11 | } 12 | 13 | // 音楽リスト 14 | .music-list__musics { 15 | width 100% 16 | border-collapse collapse 17 | border-spacing 0 18 | 19 | tr:nth-child( even ) { 20 | background-color color_gray_light 21 | } 22 | 23 | // 選択行、tr:nth-child より詳細度を高くする必要あり 24 | tr.selected { 25 | background-color color_blue_light 26 | } 27 | 28 | thead { 29 | position sticky 30 | } 31 | 32 | th { 33 | padding .3em 34 | border-right solid 1px color_gray 35 | border-bottom solid 1px color_gray 36 | font-weight normal 37 | font-size .9em 38 | } 39 | 40 | td { 41 | padding .2em .5em 42 | border-right solid 1px color_gray 43 | text-align center 44 | } 45 | 46 | .title { 47 | text-align left 48 | } 49 | 50 | .icon-speaker { 51 | font-size .7em 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /audio-player/src/stylus/Toolbar.styl: -------------------------------------------------------------------------------- 1 | @import "CommonColor.styl" 2 | 3 | toolbar_height = 48px 4 | player_width = 256px 5 | display_min_width = 256px 6 | option_width = 256px 7 | 8 | /** 9 | * ツールバー領域。 10 | */ 11 | .toolbar { 12 | position absolute 13 | top 0 14 | left 0 15 | right 0 16 | height toolbar_height 17 | border-bottom solid 1px color_gray 18 | background-color color_gray_light 19 | } 20 | 21 | // ツールバー領域内を相対配置するための領域 22 | .toolbar__wrapper { 23 | position relative 24 | width 100% 25 | height 100% 26 | } 27 | 28 | // 再生コントロール 29 | .toolbar__wrapper__player { 30 | display table-cell 31 | vertical-align middle 32 | padding 0 0 0 .5em 33 | box-sizing border-box 34 | width player_width 35 | height toolbar_height 36 | 37 | .button { 38 | display inline-block 39 | margin 0 1em 0 0 40 | font-size 1.2em 41 | } 42 | } 43 | 44 | // 音量スライダー 45 | .toolbar__wrapper__player__volume { 46 | -webkit-appearance none 47 | width 124px 48 | height 24px 49 | margin 0 50 | padding 0 51 | background-color transparent 52 | 53 | &:focus { 54 | outline none 55 | } 56 | 57 | &::-webkit-slider-thumb { 58 | -webkit-appearance none 59 | margin-top -5px 60 | width 14px 61 | height 14px 62 | border-radius 50% 63 | background-color color_gray_dark 64 | } 65 | 66 | &::-webkit-slider-runnable-track { 67 | margin-top 6px 68 | height 3px 69 | background-color color_gray 70 | } 71 | } 72 | 73 | // 音声情報の表示 74 | .toolbar__wrapper__display { 75 | position absolute 76 | top 0 77 | bottom 0 78 | left player_width 79 | right option_width 80 | min-width 16em 81 | border-left solid 1px color_gray 82 | border-right solid 1px color_gray 83 | background-color color_gray_light_ex 84 | text-align center 85 | } 86 | 87 | // 再生位置スライダー 88 | .toolbar__wrapper__display__position { 89 | -webkit-appearance none 90 | margin 0 91 | padding 0 92 | width 100% 93 | height 16px 94 | background-color transparent 95 | 96 | &:focus { 97 | outline none 98 | } 99 | 100 | &::-webkit-slider-thumb { 101 | -webkit-appearance none 102 | margin-top -8px 103 | width 8px 104 | height 12px 105 | background-color color_gray_dark 106 | } 107 | 108 | &::-webkit-slider-runnable-track { 109 | margin-top 13px 110 | height 3px 111 | background-color color_gray 112 | } 113 | } 114 | 115 | // メタデータ表示 116 | .toolbar__wrapper__display__metadata { 117 | position relative 118 | width 100% 119 | height 32px 120 | 121 | .time { 122 | position absolute 123 | bottom 0 124 | width 4em 125 | font-size .8em 126 | color color_gray_dark 127 | } 128 | 129 | .playtime { 130 | left 0 131 | padding-left .2em 132 | text-align left 133 | 134 | } 135 | 136 | .duration { 137 | right 0 138 | padding-right .2em 139 | text-align right 140 | } 141 | 142 | .title { 143 | position absolute 144 | left 4em 145 | right 4em 146 | top 0.2em 147 | text-align center 148 | } 149 | } 150 | 151 | // その他の機能 152 | .toolbar__wrapper__option { 153 | position absolute 154 | top 0 155 | right 0 156 | width option_width 157 | height toolbar_height 158 | 159 | .wrapper { 160 | display table-cell 161 | vertical-align middle 162 | width option_width 163 | height toolbar_height 164 | text-align right 165 | } 166 | 167 | .button { 168 | display inline-block 169 | margin-right 1em 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /audio-player/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/audio-player/ss.png -------------------------------------------------------------------------------- /audio-player/test/Util.test.js: -------------------------------------------------------------------------------- 1 | import Assert from 'power-assert'; 2 | import Util from '../src/js/model/Util.js'; 3 | 4 | /** @test {Util} */ 5 | describe( 'Util', () => { 6 | // 秒を時間文字列に変換 7 | /** @test {Util#secondsToString} */ 8 | describe( 'secondsToString', () => { 9 | it( 'm:ss', () => { 10 | Assert( Util.secondsToString( 5 ) === '0:05' ); 11 | } ); 12 | 13 | it( 'mm:ss', () => { 14 | Assert( Util.secondsToString( 917 ) === '15:17' ); 15 | } ); 16 | 17 | it( 'h:mm:ss', () => { 18 | Assert( Util.secondsToString( 3704 ) === '1:01:44' ); 19 | } ); 20 | 21 | it( 'hh:mm:ss', () => { 22 | Assert( Util.secondsToString( 41018 ) === '11:23:38' ); 23 | } ); 24 | } ); 25 | } ); 26 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/.gitignore: -------------------------------------------------------------------------------- 1 | src/js/jsx/*.js 2 | app.js -------------------------------------------------------------------------------- /simple-filer-without-browserify/README.md: -------------------------------------------------------------------------------- 1 | # nw.js: Use require 2 | 3 | Example of use the require in [nw.js](https://github.com/nwjs/nw.js "nw.js"). 4 | 5 | # Installation & Build 6 | 7 | 1. Install node.js, gulp and bower 8 | 1. git clone https://github.com/akabekobeko/examples-nw.git 9 | 1. cd use-require 10 | 1. npm install 11 | 1. cd src 12 | 1. npm install 13 | 1. bower install 14 | 1. cd .. 15 | 1. Run "gulp js" or "gulp release" 16 | * "gulp js" is dev build ( compile for scripts ) 17 | * "gulp release" is release build ( create node-webkit app ) 18 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require( 'gulp' ); 2 | var $ = require( 'gulp-load-plugins' )(); 3 | 4 | /** 5 | * JavaScript と JSX ファイルをコンパイルした単一ファイルを開発フォルダに出力します。 6 | * 7 | * @return {Object} gulp ストリーム。 8 | */ 9 | gulp.task( 'js', function() { 10 | return gulp.src( 'src/js/jsx/*.jsx' ) 11 | .pipe( $.react() ) 12 | .pipe( gulp.dest( 'src/js/jsx' ) ); 13 | } ); 14 | 15 | /** 16 | * リリース用イメージを削除します。 17 | * 18 | * @param {Function} cb コールバック関数。 19 | */ 20 | gulp.task( 'clean', function( cb ) { 21 | var del = require( 'del' ); 22 | del( [ 'release/bin/', 'release/src/' ], cb ); 23 | } ); 24 | 25 | /** 26 | * HTML 内のリソース参照情報を解決し、リリース用フォルダに HTML/CSS/JS を出力します。 27 | * 28 | * 対象となる JavaScript は Bower 経由でインストールしたライブラリです。 29 | * node-webkit のグローバルな require と競合せぬよう、このタスクで処理します。 30 | * 自作スクリプトは js タスクで処理されます。 31 | * 32 | * @return {Object} gulp ストリーム。 33 | */ 34 | gulp.task( 'useref', [ 'clean' ], function() { 35 | var assets = $.useref.assets(); 36 | return gulp.src( 'src/*.html' ) 37 | .pipe( assets ) 38 | .pipe( $.if( '*.css', $.minifyCss() ) ) 39 | .pipe( assets.restore() ) 40 | .pipe( $.useref() ) 41 | .pipe( gulp.dest( 'release/src' ) ); 42 | } ); 43 | 44 | /** 45 | * リリース用イメージに必要なファイルをコピーします。 46 | */ 47 | gulp.task( 'copy', [ 'js', 'useref' ], function() { 48 | return gulp.src( 49 | [ 'src/package.json', 'src/node_modules/**', 'src/fonts/**', 'src/js/*.js', 'src/js/jsx/*.js' ], 50 | { base: 'src' } 51 | ) 52 | .pipe( gulp.dest( 'release/src' ) ); 53 | } ); 54 | 55 | /** 56 | * node-webkit イメージを生成します。 57 | * 58 | * @return {Object} gulp ストリーム。 59 | */ 60 | gulp.task( 'release', [ 'copy' ], function () { 61 | var builder = require( 'node-webkit-builder' ); 62 | 63 | var nw = new builder( { 64 | version: '0.11.5', 65 | files: [ 'release/src/**' ], 66 | buildDir: 'release/bin', 67 | cacheDir: 'release/nw', 68 | platforms: [ 'osx' ] 69 | }); 70 | 71 | nw.on( 'log', function( message ) { 72 | $.util.log( 'node-webkit-builder', message ); 73 | } ); 74 | 75 | return nw.build().catch( function( err ) { 76 | $.util.log( 'node-webkit-builder', err ); 77 | } ); 78 | } ); 79 | 80 | /** 81 | * 開発用リソースの変更を監視して、必要ならビルドを実行します。 82 | */ 83 | gulp.task( 'watch', [ 'js' ], function () { 84 | gulp.watch( [ 'src/js/*.jsx', '!src/js/app.js' ], [ 'js' ]); 85 | } ); 86 | 87 | /** 88 | * gulp の既定タスクです。 89 | */ 90 | gulp.task( 'default', [ 'js' ] ); 91 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-filer-without-browserify", 3 | "version": "1.0.0", 4 | "description": "Example of use normal 'require' in nw.js.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/akabekobeko/examples-nw" 12 | }, 13 | "keywords": [ 14 | "Example", 15 | "nw.js", 16 | "require" 17 | ], 18 | "author": "akabeko", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/akabekobeko/examples-nw/issues" 22 | }, 23 | "homepage": "https://github.com/akabekobeko/examples-nw", 24 | "devDependencies": { 25 | "del": "^1.1.1", 26 | "gulp": "^3.8.10", 27 | "gulp-if": "^1.2.5", 28 | "gulp-load-plugins": "^0.8.0", 29 | "gulp-minify-css": "^0.3.13", 30 | "gulp-react": "^2.0.0", 31 | "gulp-useref": "^1.1.1", 32 | "gulp-util": "^3.0.2", 33 | "node-webkit-builder": "^1.0.6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-require", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/akabekobeko/examples-nw", 5 | "authors": [ 6 | "akabeko" 7 | ], 8 | "description": "Example of use normal 'require' in nw.js.", 9 | "keywords": [ 10 | "Example", 11 | "nw.js", 12 | "require" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "normalize.css": "~3.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/css/icomoon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "icomoon"; 3 | src:url("../fonts/icomoon.eot?-qucrg0"); 4 | src:url("../fonts/icomoon.eot?#iefix-qucrg0") format("embedded-opentype"), 5 | url("../fonts/icomoon.woff?-qucrg0") format("woff"), 6 | url("../fonts/icomoon.ttf?-qucrg0") format("truetype"), 7 | url("../fonts/icomoon.svg?-qucrg0#icomoon") format("svg"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: "icomoon"; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-file:before { 27 | content: "\e600"; 28 | } 29 | .icon-folder:before { 30 | content: "\e601"; 31 | } 32 | .icon-arrow-down:before { 33 | content: "\e602"; 34 | } 35 | .icon-arrow-right:before { 36 | content: "\e603"; 37 | } -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | /** 8 | * コンテンツ領域。 9 | */ 10 | .l-content { 11 | width: 100%; 12 | height: 100%; 13 | color: #2c3e50; 14 | } 15 | 16 | .l-content .explorer { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | overflow: hidden; 21 | } 22 | 23 | /** 24 | * ツリー内のアイコン。 25 | */ 26 | .l-content .explorer [class^="icon-"], 27 | .l-content .explorer [class*=" icon-"] { 28 | margin-right: .2em; 29 | } 30 | 31 | /** 32 | * フォルダのアイコン。 33 | */ 34 | .l-content .explorer .icon-folder { 35 | color: #3498db; 36 | } 37 | 38 | /** 39 | * ファイルのアイコン。 40 | */ 41 | .l-content .explorer .icon-file { 42 | color: #f39c12; 43 | } 44 | 45 | /******************************************************************************* 46 | * フォルダ ツリー 47 | ******************************************************************************/ 48 | 49 | /** 50 | * ツリー部分。explorer.jsx のターゲット。 51 | */ 52 | .l-content .explorer .folder-tree { 53 | position: absolute; 54 | width: 300px; 55 | left: 0; 56 | top: 0; 57 | bottom: 0; 58 | padding: .5em; 59 | box-sizing: border-box; 60 | overflow: scroll; 61 | background-color: #ecf0f1; 62 | } 63 | 64 | /** 65 | * フォルダ内のツリー。 66 | */ 67 | .l-content .explorer .folder-tree ul { 68 | margin: .5em; 69 | padding: 0 0 0 2em; 70 | list-style: none; 71 | } 72 | 73 | /** 74 | * フォルダ内ツリーのアイテム。 75 | */ 76 | .l-content .explorer .folder-tree li { 77 | padding: .2em 0; 78 | white-space: nowrap; 79 | } 80 | 81 | /** 82 | * ツリー展開状態アイコン。 83 | */ 84 | .l-content .explorer .folder-tree .icon-arrow-right, 85 | .l-content .explorer .folder-tree .icon-arrow-down { 86 | color: #95a5a6; 87 | } 88 | 89 | /******************************************************************************* 90 | * フォルダ詳細 91 | ******************************************************************************/ 92 | 93 | /** 94 | * フォルダ詳細。 95 | */ 96 | .l-content .explorer .folder-detail { 97 | position: absolute; 98 | left: 300px; 99 | top: 0; 100 | bottom: 0; 101 | right: 0; 102 | overflow: scroll; 103 | border: solid 1px #bdc3c7; 104 | } 105 | 106 | /** 107 | * 詳細テーブル。 108 | */ 109 | .l-content .explorer .folder-detail .items { 110 | width: 100%; 111 | border-collapse: collapse; 112 | border-bottom: solid 1px #bdc3c7; 113 | } 114 | 115 | /** 116 | * 選択された行。 117 | */ 118 | .l-content .explorer .folder-detail .items .selected { 119 | background-color: #ecf0f1; 120 | } 121 | 122 | /** 123 | * ヘッダー。 124 | */ 125 | .l-content .explorer .folder-detail .items th { 126 | background-color: #bdc3c7; 127 | color: #fff; 128 | font-weight: normal; 129 | padding: .2em .5em; 130 | } 131 | 132 | /** 133 | * アイテム情報。 134 | */ 135 | .l-content .explorer .folder-detail .items td { 136 | border-right: solid 1px #bdc3c7; 137 | padding: .2em .5em; 138 | white-space: nowrap; 139 | } 140 | 141 | /** 142 | * サイズ。 143 | */ 144 | .l-content .explorer .folder-detail .items td:nth-child( 3 ) { 145 | text-align: right; 146 | } 147 | 148 | /** 149 | * パーミッション。 150 | */ 151 | .l-content .explorer .folder-detail .items td:nth-child( 4 ) { 152 | text-align: center; 153 | font-family: monospace; 154 | } 155 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer-without-browserify/src/fonts/icomoon.eot -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer-without-browserify/src/fonts/icomoon.ttf -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer-without-browserify/src/fonts/icomoon.woff -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nw.js: Use require 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/js/file-utility.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * アイテム情報を生成します。 4 | * 5 | * @param {String} folderPath 親フォルダのパス。 6 | * @param {String} name アイテム名。 7 | * 8 | * @return {Object} アイテム情報。 9 | */ 10 | function createItem( folderPath, name ) { 11 | if( name.lastIndexOf( '.', 0 ) === 0 ) { return null; } 12 | 13 | var path = folderPath + name; 14 | var stat = fs.statSync( path ); 15 | var isDirectory = stat.isDirectory(); 16 | if( !( withFiles || isDirectory ) ) { return null; } 17 | 18 | return { 19 | name: name, 20 | path: path, 21 | size: stat.size, 22 | mtime: stat.mtime, 23 | isDirectory: isDirectory 24 | }; 25 | } 26 | 27 | module.exports = { 28 | /** 29 | * フォルダ内のアイテムを列挙します。 30 | * 31 | * @param {String} folderPath フォルダのパス。 32 | * @param {Function} onEnd フォルダを列挙し終えたことを通知するコールバック関数。 33 | * @param {Boolean} withFiles ファイルも列挙する場合は true。既定はフォルダのみ。 34 | */ 35 | enumItemsAtFolder: function( folderPath, onEnd, withFiles ) { 36 | folderPath += '/'; 37 | 38 | var fs = require( 'fs' ); 39 | fs.readdir( folderPath, function( err, names ) { 40 | if( err ) { 41 | console.log( err ); 42 | onEnd( [] ); 43 | return; 44 | } 45 | 46 | var items = []; 47 | names.forEach( function( name, index ) { 48 | function createItem() { 49 | if( name.lastIndexOf( '.', 0 ) === 0 ) { return null; } 50 | 51 | var path = folderPath + name; 52 | var stat = fs.statSync( path ); 53 | var isDirectory = stat.isDirectory(); 54 | if( !( withFiles || isDirectory ) ) { return null; } 55 | 56 | return { 57 | name: name, 58 | path: path, 59 | size: stat.size, 60 | mode: stat.mode, 61 | mtime: stat.mtime, 62 | isDirectory: isDirectory 63 | }; 64 | } 65 | 66 | var item = createItem(); 67 | if( item ) { 68 | items.push( item ); 69 | } 70 | 71 | if( index === names.length - 1 ) { 72 | onEnd( items ); 73 | } 74 | } ); 75 | } ); 76 | }, 77 | /** 78 | * バイト数を単位付き文字列に変換します。 79 | * 80 | * @param {Number} size ファイル サイズ。 81 | * 82 | * @return {String} 単位付きのファイル サイズ文字列。TB を超えるサイズの場合は '--' を返します。 83 | */ 84 | bytesToSize: function( bytes ) { 85 | if( bytes === 0 ) { return '--'; } 86 | 87 | var k = 1024; 88 | var units = [ 'Bytes', 'KB', 'MB', 'GB', 'TB' ]; 89 | var unit = parseInt( Math.floor( Math.log( bytes ) / Math.log( k ) ) ); 90 | var size = ( bytes / Math.pow( k, unit ) ) * 10; 91 | 92 | return ( Math.ceil( size ) / 10 )+ ' ' + units[ unit ]; 93 | }, 94 | /** 95 | * 日時情報を文字列化します。 96 | * 97 | * @param {Date} date 日時情報。 98 | * 99 | * @return {String} 文字列。 100 | */ 101 | dateToString: function( date ) { 102 | return ( date && date.toLocaleDateString ? date.toLocaleDateString() : '' ); 103 | }, 104 | /** 105 | * アイテム種別を取得します。 106 | * 107 | * @param {[type]} item [description] 108 | * @return {[type]} [description] 109 | */ 110 | getItemType: function( item ) { 111 | if( item.isDirectory ) { return 'Folder'; } 112 | 113 | var path = require( 'path' ); 114 | var ext = path.extname( item.path ); 115 | switch( ext ) { 116 | case '.txt': return 'Text'; 117 | case '.md': return 'Markdown'; 118 | case '.html': return 'HTML'; 119 | case '.css': return 'Style Sheet'; 120 | case '.js': return 'JavaScript'; 121 | case '.jpeg': return 'JPEG'; 122 | case '.png': return 'PNG'; 123 | case '.gif': return 'GIF'; 124 | case '.mp3': return 'MPEG3'; 125 | case '.mp4': return 'MPEG4'; 126 | case '.aac': return 'AAC'; 127 | default: return 'File'; 128 | } 129 | }, 130 | 131 | /** 132 | * ユーザーのホームディレクトリを取得します。 133 | * http://stackoverflow.com/questions/9080085/node-js-find-home-directory-in-platform-agnostic-way 134 | * 135 | * @return {String} ホームディレクトリのパス。 136 | */ 137 | getUserHomeDir: function() { 138 | return process.env[ ( process.platform == 'win32' ) ? 'USERPROFILE' : 'HOME' ]; 139 | }, 140 | /** 141 | * パーミッションを示す数値から記号化された文字列を取得します。 142 | * 143 | * @param {Number} mode モード。 144 | * 145 | * @return {String} パーミッション表記の文字列。 146 | */ 147 | getPermissionString: function( mode, isDirectory ) { 148 | var S_IRUSR = 0x0400; 149 | var S_IWUSR = 0x0200; 150 | var S_IXUSR = 0x0100; 151 | var S_IRGRP = 0x0040; 152 | var S_IWGRP = 0x0020; 153 | var S_IXGRP = 0x0010; 154 | var S_IROTH = 0x0004; 155 | var S_IWOTH = 0x0002; 156 | var S_IXOTH = 0x0001; 157 | 158 | var str = 159 | ( isDirectory ? 'd' : '-' ) + 160 | ( mode & S_IRUSR ? 'r' : '-' ) + 161 | ( mode & S_IWUSR ? 'w' : '-' ) + 162 | ( mode & S_IXUSR ? 'x' : '-' ) + 163 | ( mode & S_IRGRP ? 'r' : '-' ) + 164 | ( mode & S_IWGRP ? 'w' : '-' ) + 165 | ( mode & S_IXGRP ? 'x' : '-' ) + 166 | ( mode & S_IROTH ? 'r' : '-' ) + 167 | ( mode & S_IWOTH ? 'w' : '-' ) + 168 | ( mode & S_IXOTH ? 'x' : '-' ); 169 | 170 | return str; 171 | }, 172 | shellOpenItem: function( path ) { 173 | var gui = window.require( 'nw.gui' ); 174 | gui.Shell.openItem( path ); 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/js/jsx/explorer.jsx: -------------------------------------------------------------------------------- 1 | var React = require( 'react' ); 2 | var FolderTree = require( './folder-tree' ); 3 | var FolderDetail = require( './folder-detail' ); 4 | 5 | /** 6 | * ファイル、フォルダのビューアーです。 7 | */ 8 | var Explorer = React.createClass( { 9 | /** 10 | * コンポーネントの状態を初期化します。 11 | * 12 | * @return {Object} 初期化された状態オブジェクト。 13 | */ 14 | getInitialState: function() { 15 | var fileutil = require( '../file-utility' ); 16 | return { 17 | currentFolder: fileutil.getUserHomeDir(), 18 | items: [] 19 | }; 20 | }, 21 | /** 22 | * コンポーネントが DOM ツリーへ追加された時に発生します。 23 | */ 24 | componentWillMount: function() { 25 | this.updateFolderDetail( this.state.currentFolder ); 26 | }, 27 | /** 28 | * コンポーネントの描画オブジェクトを取得します。 29 | * 30 | * @return {Object} 描画オブジェクト。 31 | */ 32 | render: function() { 33 | return ( 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | ); 43 | }, 44 | /** 45 | * フォルダ詳細を更新します。 46 | * 47 | * @param {String} folder 新たに選択されたフォルダ。 48 | */ 49 | updateFolderDetail: function( folder ) { 50 | var fileutil = require( '../file-utility' ); 51 | var component = this; 52 | 53 | fileutil.enumItemsAtFolder( 54 | folder, 55 | function( items ) { 56 | items.sort( function( a, b ) { 57 | return ( a.isDirectory === b.isDirectory ? a.name.localeCompare( b.name ) : ( a.isDirectory ? -1 : 1 ) ); 58 | } ); 59 | 60 | component.setState( { currentFolder: folder, items: items } ); 61 | }, 62 | true 63 | ); 64 | }, 65 | /** 66 | * フォルダが選択された時に発生します。 67 | * 68 | * @param {String} folder 選択されたフォルダ。 69 | */ 70 | onSelectFolder: function( folder ) { 71 | if( folder !== this.state.currentFolder ) { 72 | this.updateFolderDetail( folder ); 73 | } 74 | } 75 | } ); 76 | 77 | module.exports = function( target ) { 78 | React.render( 79 | , 80 | document.querySelector( target ) 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/js/jsx/folder-detail.jsx: -------------------------------------------------------------------------------- 1 | var React = require( 'react' ); 2 | 3 | /** 4 | * フォルダー内の詳細情報コンポーネントです。 5 | */ 6 | var FolderDetail = React.createClass( { 7 | /** 8 | * コンポーネントの状態を初期化します。 9 | * 10 | * @return {Object} 初期化された状態オブジェクト。 11 | */ 12 | getInitialState: function() { 13 | return { 14 | selectedItem: null 15 | }; 16 | }, 17 | /** 18 | * コンポーネントの描画オブジェクトを取得します。 19 | * 20 | * @return {Object} 描画オブジェクト。 21 | */ 22 | render: function() { 23 | var fileutil = require( '../file-utility' ); 24 | 25 | var items = this.props.items.map( function( item, index ) { 26 | var style = ( item === this.state.selectedItem ? 'selected' : '' ); 27 | var icon = ( item.isDirectory ? 'icon-folder' : 'icon-file' ); 28 | var type = fileutil.getItemType( item ); 29 | var size = fileutil.bytesToSize( item.size ); 30 | var mode = fileutil.getPermissionString( item.mode, item.isDirectory ); 31 | var date = fileutil.dateToString( item.mtime ); 32 | 33 | return ( 34 | 39 | {item.name} 40 | {type} 41 | {size} 42 | {mode} 43 | {date} 44 | 45 | ); 46 | }, this ); 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | {items} 55 | 56 |
NameTypeSizePermissionModified
57 | ); 58 | }, 59 | /** 60 | * アイテムがクリックされた時に発生します。 61 | * 62 | * @param {Object} アイテム情報。 63 | */ 64 | onClickItem: function( item ) { 65 | this.setState( { selectedItem: item } ); 66 | }, 67 | /** 68 | * アイテムがダブル クリックされた時に発生します。 69 | * 70 | * @param {Object} アイテム情報。 71 | */ 72 | onDoubleClickItem: function( item ) { 73 | if( item.isDirectory ) { 74 | // ここでフォルダ ツリーに変更通知して展開させたい 75 | 76 | } else { 77 | var fileutil = require( '../file-utility' ); 78 | fileutil.shellOpenItem( item.path ); 79 | } 80 | } 81 | } ); 82 | 83 | module.exports = FolderDetail; 84 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/js/jsx/folder-tree.jsx: -------------------------------------------------------------------------------- 1 | var React = require( 'react' ); 2 | 3 | /** 4 | * フォルダー ツリーとなるコンポーネントです。 5 | */ 6 | var FolderTree = React.createClass( { 7 | /** 8 | * コンポーネントの状態を初期化します。 9 | * 10 | * @return {Object} 初期化された状態オブジェクト。 11 | */ 12 | getInitialState: function() { 13 | return { 14 | expanded: false, 15 | enumerated: false 16 | }; 17 | }, 18 | /** 19 | * コンポーネントの描画オブジェクトを取得します。 20 | * 21 | * @return {Object} 描画オブジェクト。 22 | */ 23 | render: function() { 24 | var subFolders = null; 25 | if( this.state.subFolders ) { 26 | var onSelectFolder = this.props.onSelectFolder; 27 | subFolders = this.state.subFolders.map( function( item, index ) { 28 | return (
  • ); 29 | } ); 30 | } 31 | 32 | var style = this.state.expanded ? {} : { display: 'none' }; 33 | var mark = this.state.expanded ? 'icon-arrow-down' : 'icon-arrow-right'; 34 | return ( 35 |
    36 |
    37 | 38 | 39 | {this.props.name} 40 |
    41 |
      42 | {subFolders} 43 |
    44 |
    45 | ); 46 | }, 47 | /** 48 | * アイテムがクリックされた時に発生します。 49 | */ 50 | onClick: function() { 51 | if( this.state.enumerated ) { 52 | this.setState( { expanded: !this.state.expanded } ); 53 | this.props.onSelectFolder( this.props.path ); 54 | 55 | } else { 56 | this.setState( { enumerated: true } ); 57 | 58 | var component = this; 59 | var fileutil = require( '../file-utility' ); 60 | 61 | fileutil.enumItemsAtFolder( this.props.path, function( subFolders ) { 62 | component.setState( { 63 | subFolders: subFolders, 64 | expanded: !component.state.expanded 65 | } ); 66 | 67 | component.props.onSelectFolder( component.props.path ); 68 | } ); 69 | } 70 | } 71 | } ); 72 | 73 | module.exports = FolderTree; 74 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | global.document = window.document; 3 | global.navigator = window.navigator; 4 | 5 | var explorer = require( './js/jsx/explorer' ); 6 | explorer( '.l-content' ); 7 | -------------------------------------------------------------------------------- /simple-filer-without-browserify/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-filer-without-browserify", 3 | "version": "1.0.0", 4 | "description": "Example of use normal 'require' in nw.js.", 5 | "main": "index.html", 6 | "window": { 7 | "title": "Simple filer without browserify", 8 | "toolbar": true, 9 | "frame": true, 10 | "resizable": true, 11 | "width": 800, 12 | "height": 480, 13 | "position": "mouse" 14 | }, 15 | "platformOverrides": { 16 | "osx": { 17 | "window": { 18 | "toolbar": false 19 | } 20 | }, 21 | "win": { 22 | "window": { 23 | "toolbar": false 24 | } 25 | }, 26 | "linux": { 27 | "window": { 28 | "toolbar": false 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /simple-filer/.gitignore: -------------------------------------------------------------------------------- 1 | app.js 2 | -------------------------------------------------------------------------------- /simple-filer/README.md: -------------------------------------------------------------------------------- 1 | # nw.js: Simple Filer 2 | 3 | Example of the simple filer in [nw.js](https://github.com/nwjs/nw.js "nw.js"). 4 | 5 | ![Screenshot](ss.png) 6 | 7 | # Installation & Build 8 | 9 | 1. Install node.js, gulp and bower 10 | 1. git clone https://github.com/akabekobeko/examples-nw.git 11 | 1. cd simple-filer 12 | 1. npm install 13 | 1. cd src 14 | 1. bower install 15 | 1. cd .. 16 | 1. Run "gulp js" or "gulp release" 17 | * "gulp js" is dev build ( compile for scripts ) 18 | * "gulp release" is release build ( create node-webkit app ) 19 | -------------------------------------------------------------------------------- /simple-filer/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require( 'gulp' ); 2 | var $ = require( 'gulp-load-plugins' )(); 3 | 4 | /** 5 | * JavaScript と JSX ファイルをコンパイルした単一ファイルを開発フォルダに出力します。 6 | * 7 | * @return {Object} gulp ストリーム。 8 | */ 9 | gulp.task( 'js', function() { 10 | var browserify = require( 'browserify' ); 11 | var source = require( 'vinyl-source-stream' ); 12 | var buffer = require( 'vinyl-buffer' ); 13 | 14 | return browserify( 15 | './src/js/main.js', 16 | { 17 | debug: true, 18 | detectGlobals: false, 19 | builtins: [], 20 | transform: [ 'reactify' ] 21 | } 22 | ) 23 | .bundle() 24 | .pipe( source( 'app.js' ) ) 25 | .pipe( buffer() ) 26 | .pipe( $.sourcemaps.init( { loadMaps: true } ) ) 27 | .pipe( $.replace( 'require', 'requireClient' ) ) 28 | .pipe( $.replace( 'nequire', 'require' ) ) 29 | //.pipe( $.uglify() ) 30 | .pipe( $.sourcemaps.write( './' ) ) 31 | .pipe( gulp.dest( 'src/js' ) ); 32 | } ); 33 | 34 | /** 35 | * リリース用イメージを削除します。 36 | * 37 | * @param {Function} cb コールバック関数。 38 | */ 39 | gulp.task( 'clean', function( cb ) { 40 | var del = require( 'del' ); 41 | del( [ 'release/bin/', 'release/src/' ], cb ); 42 | } ); 43 | 44 | /** 45 | * HTML 内のリソース参照情報を解決し、リリース用フォルダに HTML/CSS/JS を出力します。 46 | * 47 | * 対象となる JavaScript は Bower 経由でインストールしたライブラリです。 48 | * node-webkit のグローバルな require と競合せぬよう、このタスクで処理します。 49 | * 自作スクリプトは js タスクで処理されます。 50 | * 51 | * @return {Object} gulp ストリーム。 52 | */ 53 | gulp.task( 'useref', [ 'clean' ], function() { 54 | var assets = $.useref.assets(); 55 | return gulp.src( 'src/*.html' ) 56 | .pipe( assets ) 57 | .pipe( $.if( '*.css', $.minifyCss() ) ) 58 | .pipe( assets.restore() ) 59 | .pipe( $.useref() ) 60 | .pipe( gulp.dest( 'release/src' ) ); 61 | } ); 62 | 63 | /** 64 | * リリース用イメージに必要なファイルをコピーします。 65 | */ 66 | gulp.task( 'copy', [ 'js', 'useref' ], function() { 67 | return gulp.src( 68 | [ 'src/fonts/**', 'src/js/app.js', 'src/package.json' ], 69 | { base: 'src' } 70 | ) 71 | .pipe( gulp.dest( 'release/src' ) ); 72 | } ); 73 | 74 | /** 75 | * node-webkit イメージを生成します。 76 | * 77 | * @return {Object} gulp ストリーム。 78 | */ 79 | gulp.task( 'release', [ 'copy' ], function () { 80 | var builder = require( 'node-webkit-builder' ); 81 | 82 | var nw = new builder( { 83 | version: '0.11.5', 84 | files: [ 'release/src/**' ], 85 | buildDir: 'release/bin', 86 | cacheDir: 'release/nw', 87 | platforms: [ 'osx' ] 88 | }); 89 | 90 | nw.on( 'log', function( message ) { 91 | $.util.log( 'node-webkit-builder', message ); 92 | } ); 93 | 94 | return nw.build().catch( function( err ) { 95 | $.util.log( 'node-webkit-builder', err ); 96 | } ); 97 | } ); 98 | 99 | /** 100 | * 開発用リソースの変更を監視して、必要ならビルドを実行します。 101 | */ 102 | gulp.task( 'watch', [ 'js' ], function () { 103 | gulp.watch( [ 'src/js/*.js', '!src/js/app.js' ], [ 'js' ]); 104 | } ); 105 | 106 | /** 107 | * gulp の既定タスクです。 108 | */ 109 | gulp.task( 'default', [ 'js' ] ); 110 | -------------------------------------------------------------------------------- /simple-filer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-filer", 3 | "version": "1.0.0", 4 | "description": "Example of the simple filer in node-webkit.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/akabekobeko/examples-nw" 12 | }, 13 | "keywords": [ 14 | "Example", 15 | "node-webkit", 16 | "Filer" 17 | ], 18 | "author": "akabeko", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/akabekobeko/examples-nw/issues" 22 | }, 23 | "homepage": "https://github.com/akabekobeko/examples-nw", 24 | "devDependencies": { 25 | "browserify": "^8.0.3", 26 | "del": "^1.1.1", 27 | "gulp": "^3.8.10", 28 | "gulp-if": "^1.2.5", 29 | "gulp-load-plugins": "^0.8.0", 30 | "gulp-minify-css": "^0.3.11", 31 | "gulp-replace": "^0.5.0", 32 | "gulp-sourcemaps": "^1.3.0", 33 | "gulp-uglify": "^1.0.2", 34 | "gulp-useref": "^1.1.0", 35 | "gulp-util": "^3.0.1", 36 | "node-webkit-builder": "^1.0.4", 37 | "reactify": "^0.17.1", 38 | "vinyl-buffer": "^1.0.0", 39 | "vinyl-source-stream": "^1.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /simple-filer/src/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-filer", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/akabekobeko/examples-nw", 5 | "authors": [ 6 | "akabeko" 7 | ], 8 | "description": "Example of the simple filer in node-webkit.", 9 | "keywords": [ 10 | "Example", 11 | "node-webkit", 12 | "Filer" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "normalize.css": "~3.0.2", 24 | "react": "~0.12.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /simple-filer/src/css/icomoon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "icomoon"; 3 | src:url("../fonts/icomoon.eot?-qucrg0"); 4 | src:url("../fonts/icomoon.eot?#iefix-qucrg0") format("embedded-opentype"), 5 | url("../fonts/icomoon.woff?-qucrg0") format("woff"), 6 | url("../fonts/icomoon.ttf?-qucrg0") format("truetype"), 7 | url("../fonts/icomoon.svg?-qucrg0#icomoon") format("svg"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: "icomoon"; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-file:before { 27 | content: "\e600"; 28 | } 29 | .icon-folder:before { 30 | content: "\e601"; 31 | } 32 | .icon-arrow-down:before { 33 | content: "\e602"; 34 | } 35 | .icon-arrow-right:before { 36 | content: "\e603"; 37 | } -------------------------------------------------------------------------------- /simple-filer/src/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | /** 8 | * コンテンツ領域。 9 | */ 10 | .l-content { 11 | width: 100%; 12 | height: 100%; 13 | color: #2c3e50; 14 | } 15 | 16 | .l-content .explorer { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | overflow: hidden; 21 | } 22 | 23 | /** 24 | * ツリー内のアイコン。 25 | */ 26 | .l-content .explorer [class^="icon-"], 27 | .l-content .explorer [class*=" icon-"] { 28 | margin-right: .2em; 29 | } 30 | 31 | /** 32 | * フォルダのアイコン。 33 | */ 34 | .l-content .explorer .icon-folder { 35 | color: #3498db; 36 | } 37 | 38 | /** 39 | * ファイルのアイコン。 40 | */ 41 | .l-content .explorer .icon-file { 42 | color: #f39c12; 43 | } 44 | 45 | /******************************************************************************* 46 | * フォルダ ツリー 47 | ******************************************************************************/ 48 | 49 | /** 50 | * ツリー部分。explorer.jsx のターゲット。 51 | */ 52 | .l-content .explorer .folder-tree { 53 | position: absolute; 54 | width: 300px; 55 | left: 0; 56 | top: 0; 57 | bottom: 0; 58 | padding: .5em; 59 | box-sizing: border-box; 60 | overflow: scroll; 61 | background-color: #ecf0f1; 62 | } 63 | 64 | /** 65 | * フォルダ内のツリー。 66 | */ 67 | .l-content .explorer .folder-tree ul { 68 | margin: .5em; 69 | padding: 0 0 0 2em; 70 | list-style: none; 71 | } 72 | 73 | /** 74 | * フォルダ内ツリーのアイテム。 75 | */ 76 | .l-content .explorer .folder-tree li { 77 | padding: .2em 0; 78 | white-space: nowrap; 79 | } 80 | 81 | /** 82 | * ツリー展開状態アイコン。 83 | */ 84 | .l-content .explorer .folder-tree .icon-arrow-right, 85 | .l-content .explorer .folder-tree .icon-arrow-down { 86 | color: #95a5a6; 87 | } 88 | 89 | /******************************************************************************* 90 | * フォルダ詳細 91 | ******************************************************************************/ 92 | 93 | /** 94 | * フォルダ詳細。 95 | */ 96 | .l-content .explorer .folder-detail { 97 | position: absolute; 98 | left: 300px; 99 | top: 0; 100 | bottom: 0; 101 | right: 0; 102 | overflow: scroll; 103 | border: solid 1px #bdc3c7; 104 | } 105 | 106 | /** 107 | * 詳細テーブル。 108 | */ 109 | .l-content .explorer .folder-detail .items { 110 | width: 100%; 111 | border-collapse: collapse; 112 | border-bottom: solid 1px #bdc3c7; 113 | } 114 | 115 | /** 116 | * 選択された行。 117 | */ 118 | .l-content .explorer .folder-detail .items .selected { 119 | background-color: #ecf0f1; 120 | } 121 | 122 | /** 123 | * ヘッダー。 124 | */ 125 | .l-content .explorer .folder-detail .items th { 126 | background-color: #bdc3c7; 127 | color: #fff; 128 | font-weight: normal; 129 | padding: .2em .5em; 130 | } 131 | 132 | /** 133 | * アイテム情報。 134 | */ 135 | .l-content .explorer .folder-detail .items td { 136 | border-right: solid 1px #bdc3c7; 137 | padding: .2em .5em; 138 | white-space: nowrap; 139 | } 140 | 141 | /** 142 | * サイズ。 143 | */ 144 | .l-content .explorer .folder-detail .items td:nth-child( 3 ) { 145 | text-align: right; 146 | } 147 | 148 | /** 149 | * パーミッション。 150 | */ 151 | .l-content .explorer .folder-detail .items td:nth-child( 4 ) { 152 | text-align: center; 153 | font-family: monospace; 154 | } 155 | -------------------------------------------------------------------------------- /simple-filer/src/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer/src/fonts/icomoon.eot -------------------------------------------------------------------------------- /simple-filer/src/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /simple-filer/src/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer/src/fonts/icomoon.ttf -------------------------------------------------------------------------------- /simple-filer/src/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer/src/fonts/icomoon.woff -------------------------------------------------------------------------------- /simple-filer/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | node-webkit: Simple Filer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /simple-filer/src/js/explorer.jsx: -------------------------------------------------------------------------------- 1 | 2 | var FolderTree = require( './folder-tree.jsx' ); 3 | var FolderDetail = require( './folder-detail.jsx' ); 4 | 5 | /** 6 | * ファイル、フォルダのビューアーです。 7 | */ 8 | var Explorer = React.createClass( { 9 | /** 10 | * コンポーネントの状態を初期化します。 11 | * 12 | * @return {Object} 初期化された状態オブジェクト。 13 | */ 14 | getInitialState: function() { 15 | var fileutil = require( './file-utility.js' ); 16 | return { 17 | currentFolder: fileutil.getUserHomeDir(), 18 | items: [] 19 | }; 20 | }, 21 | /** 22 | * コンポーネントが DOM ツリーへ追加された時に発生します。 23 | */ 24 | componentWillMount: function() { 25 | this.updateFolderDetail( this.state.currentFolder ); 26 | }, 27 | /** 28 | * コンポーネントの描画オブジェクトを取得します。 29 | * 30 | * @return {Object} 描画オブジェクト。 31 | */ 32 | render: function() { 33 | return ( 34 |
    35 |
    36 | 37 |
    38 |
    39 | 40 |
    41 |
    42 | ); 43 | }, 44 | /** 45 | * フォルダ詳細を更新します。 46 | * 47 | * @param {String} folder 新たに選択されたフォルダ。 48 | */ 49 | updateFolderDetail: function( folder ) { 50 | var fileutil = require( './file-utility.js' ); 51 | var component = this; 52 | 53 | fileutil.enumItemsAtFolder( 54 | folder, 55 | function( items ) { 56 | items.sort( function( a, b ) { 57 | return ( a.isDirectory === b.isDirectory ? a.name.localeCompare( b.name ) : ( a.isDirectory ? -1 : 1 ) ); 58 | } ); 59 | 60 | component.setState( { currentFolder: folder, items: items } ); 61 | }, 62 | true 63 | ); 64 | }, 65 | /** 66 | * フォルダが選択された時に発生します。 67 | * 68 | * @param {String} folder 選択されたフォルダ。 69 | */ 70 | onSelectFolder: function( folder ) { 71 | if( folder !== this.state.currentFolder ) { 72 | this.updateFolderDetail( folder ); 73 | } 74 | } 75 | } ); 76 | 77 | module.exports = function( target ) { 78 | React.render( 79 | , 80 | document.querySelector( target ) 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /simple-filer/src/js/file-utility.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * アイテム情報を生成します。 4 | * 5 | * @param {String} folderPath 親フォルダのパス。 6 | * @param {String} name アイテム名。 7 | * 8 | * @return {Object} アイテム情報。 9 | */ 10 | function createItem( folderPath, name ) { 11 | if( name.lastIndexOf( '.', 0 ) === 0 ) { return null; } 12 | 13 | var path = folderPath + name; 14 | var stat = fs.statSync( path ); 15 | var isDirectory = stat.isDirectory(); 16 | if( !( withFiles || isDirectory ) ) { return null; } 17 | 18 | return { 19 | name: name, 20 | path: path, 21 | size: stat.size, 22 | mtime: stat.mtime, 23 | isDirectory: isDirectory 24 | }; 25 | } 26 | 27 | module.exports = { 28 | /** 29 | * フォルダ内のアイテムを列挙します。 30 | * 31 | * @param {String} folderPath フォルダのパス。 32 | * @param {Function} onEnd フォルダを列挙し終えたことを通知するコールバック関数。 33 | * @param {Boolean} withFiles ファイルも列挙する場合は true。既定はフォルダのみ。 34 | */ 35 | enumItemsAtFolder: function( folderPath, onEnd, withFiles ) { 36 | folderPath += '/'; 37 | 38 | var fs = nequire( 'fs' ); 39 | fs.readdir( folderPath, function( err, names ) { 40 | if( err ) { 41 | console.log( err ); 42 | onEnd( [] ); 43 | return; 44 | } 45 | 46 | var items = []; 47 | names.forEach( function( name, index ) { 48 | function createItem() { 49 | if( name.lastIndexOf( '.', 0 ) === 0 ) { return null; } 50 | 51 | var path = folderPath + name; 52 | var stat = fs.statSync( path ); 53 | var isDirectory = stat.isDirectory(); 54 | if( !( withFiles || isDirectory ) ) { return null; } 55 | 56 | return { 57 | name: name, 58 | path: path, 59 | size: stat.size, 60 | mode: stat.mode, 61 | mtime: stat.mtime, 62 | isDirectory: isDirectory 63 | }; 64 | } 65 | 66 | var item = createItem(); 67 | if( item ) { 68 | items.push( item ); 69 | } 70 | 71 | if( index === names.length - 1 ) { 72 | onEnd( items ); 73 | } 74 | } ); 75 | } ); 76 | }, 77 | /** 78 | * バイト数を単位付き文字列に変換します。 79 | * 80 | * @param {Number} size ファイル サイズ。 81 | * 82 | * @return {String} 単位付きのファイル サイズ文字列。TB を超えるサイズの場合は '--' を返します。 83 | */ 84 | bytesToSize: function( bytes ) { 85 | if( bytes === 0 ) { return '--'; } 86 | 87 | var k = 1024; 88 | var units = [ 'Bytes', 'KB', 'MB', 'GB', 'TB' ]; 89 | var unit = parseInt( Math.floor( Math.log( bytes ) / Math.log( k ) ) ); 90 | var size = ( bytes / Math.pow( k, unit ) ) * 10; 91 | 92 | return ( Math.ceil( size ) / 10 )+ ' ' + units[ unit ]; 93 | }, 94 | /** 95 | * ユーザーのホームディレクトリを取得します。 96 | * http://stackoverflow.com/questions/9080085/node-js-find-home-directory-in-platform-agnostic-way 97 | * 98 | * @return {String} ホームディレクトリのパス。 99 | */ 100 | getUserHomeDir: function() { 101 | return process.env[ ( process.platform == 'win32' ) ? 'USERPROFILE' : 'HOME' ]; 102 | }, 103 | /** 104 | * パーミッションを示す数値から記号化された文字列を取得します。 105 | * 106 | * @param {Number} mode モード。 107 | * 108 | * @return {String} パーミッション表記の文字列。 109 | */ 110 | getPermissionString: function( mode, isDirectory ) { 111 | var S_IRUSR = 0x0400; 112 | var S_IWUSR = 0x0200; 113 | var S_IXUSR = 0x0100; 114 | var S_IRGRP = 0x0040; 115 | var S_IWGRP = 0x0020; 116 | var S_IXGRP = 0x0010; 117 | var S_IROTH = 0x0004; 118 | var S_IWOTH = 0x0002; 119 | var S_IXOTH = 0x0001; 120 | 121 | var str = 122 | ( isDirectory ? 'd' : '-' ) + 123 | ( mode & S_IRUSR ? 'r' : '-' ) + 124 | ( mode & S_IWUSR ? 'w' : '-' ) + 125 | ( mode & S_IXUSR ? 'x' : '-' ) + 126 | ( mode & S_IRGRP ? 'r' : '-' ) + 127 | ( mode & S_IWGRP ? 'w' : '-' ) + 128 | ( mode & S_IXGRP ? 'x' : '-' ) + 129 | ( mode & S_IROTH ? 'r' : '-' ) + 130 | ( mode & S_IWOTH ? 'w' : '-' ) + 131 | ( mode & S_IXOTH ? 'x' : '-' ); 132 | 133 | return str; 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /simple-filer/src/js/folder-detail.jsx: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * アイテム種別を取得します。 4 | * 5 | * @param {[type]} item [description] 6 | * @return {[type]} [description] 7 | */ 8 | function getItemType( item ) { 9 | if( item.isDirectory ) { return 'Folder'; } 10 | 11 | var path = nequire( 'path' ); 12 | var ext = path.extname( item.path ); 13 | switch( ext ) { 14 | case '.txt': return 'Text'; 15 | case '.md': return 'Markdown'; 16 | case '.html': return 'HTML'; 17 | case '.css': return 'Style Sheet'; 18 | case '.js': return 'JavaScript'; 19 | case '.jpeg': return 'JPEG'; 20 | case '.png': return 'PNG'; 21 | case '.gif': return 'GIF'; 22 | case '.mp3': return 'MPEG3'; 23 | case '.mp4': return 'MPEG4'; 24 | case '.aac': return 'AAC'; 25 | default: return 'File'; 26 | } 27 | } 28 | 29 | /** 30 | * 日時情報を文字列化します。 31 | * 32 | * @param {Date} date 日時情報。 33 | * 34 | * @return {String} 文字列。 35 | */ 36 | function dateToString( date ) { 37 | return ( date && date.toLocaleDateString ? date.toLocaleDateString() : '' ); 38 | } 39 | 40 | /** 41 | * フォルダー内の詳細情報コンポーネントです。 42 | */ 43 | var FolderDetail = React.createClass( { 44 | /** 45 | * コンポーネントの状態を初期化します。 46 | * 47 | * @return {Object} 初期化された状態オブジェクト。 48 | */ 49 | getInitialState: function() { 50 | return { 51 | selectedItem: null 52 | }; 53 | }, 54 | /** 55 | * コンポーネントの描画オブジェクトを取得します。 56 | * 57 | * @return {Object} 描画オブジェクト。 58 | */ 59 | render: function() { 60 | var fileutil = require( './file-utility.js' ); 61 | var component = this; 62 | 63 | var items = this.props.items.map( function( item, index ) { 64 | var style = ( item === component.state.selectedItem ? 'selected' : '' ); 65 | var icon = ( item.isDirectory ? 'icon-folder' : 'icon-file' ); 66 | var type = getItemType( item ); 67 | var size = fileutil.bytesToSize( item.size ); 68 | var mode = fileutil.getPermissionString( item.mode, item.isDirectory ); 69 | var date = dateToString( item.mtime ); 70 | 71 | return ( 72 | 76 | {item.name} 77 | {type} 78 | {size} 79 | {mode} 80 | {date} 81 | 82 | ); 83 | } ); 84 | 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | {items} 92 | 93 |
    NameTypeSizePermissionModified
    94 | ); 95 | }, 96 | /** 97 | * アイテムがクリックされた時に発生します。 98 | * 99 | * @param {Object} アイテム情報。 100 | */ 101 | onClickItem: function( item ) { 102 | this.setState( { selectedItem: item } ); 103 | }, 104 | /** 105 | * アイテムがダブル クリックされた時に発生します。 106 | * 107 | * @param {Object} アイテム情報。 108 | */ 109 | onDoubleClickItem: function( item ) { 110 | if( item.isDirectory ) { 111 | // ここでフォルダ ツリーに変更通知して展開させたい 112 | 113 | } else { 114 | var gui = nequire( 'nw.gui' ); 115 | gui.Shell.openItem( item.path ); 116 | } 117 | } 118 | } ); 119 | 120 | module.exports = FolderDetail; 121 | -------------------------------------------------------------------------------- /simple-filer/src/js/folder-tree.jsx: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * フォルダー ツリーとなるコンポーネントです。 4 | */ 5 | var FolderTree = React.createClass( { 6 | /** 7 | * コンポーネントの状態を初期化します。 8 | * 9 | * @return {Object} 初期化された状態オブジェクト。 10 | */ 11 | getInitialState: function() { 12 | return { 13 | expanded: false, 14 | enumerated: false 15 | }; 16 | }, 17 | /** 18 | * コンポーネントの描画オブジェクトを取得します。 19 | * 20 | * @return {Object} 描画オブジェクト。 21 | */ 22 | render: function() { 23 | var subFolders = null; 24 | if( this.state.subFolders ) { 25 | var onSelectFolder = this.props.onSelectFolder; 26 | subFolders = this.state.subFolders.map( function( item, index ) { 27 | return (
  • ); 28 | } ); 29 | } 30 | 31 | var style = this.state.expanded ? {} : { display: 'none' }; 32 | var mark = this.state.expanded ? 'icon-arrow-down' : 'icon-arrow-right'; 33 | return ( 34 |
    35 |
    36 | 37 | 38 | {this.props.name} 39 |
    40 |
      41 | {subFolders} 42 |
    43 |
    44 | ); 45 | }, 46 | /** 47 | * アイテムがクリックされた時に発生します。 48 | */ 49 | onClick: function() { 50 | if( this.state.enumerated ) { 51 | this.setState( { expanded: !this.state.expanded } ); 52 | this.props.onSelectFolder( this.props.path ); 53 | 54 | } else { 55 | this.setState( { enumerated: true } ); 56 | 57 | var component = this; 58 | var fileutil = require( './file-utility.js' ); 59 | 60 | fileutil.enumItemsAtFolder( this.props.path, function( subFolders ) { 61 | component.setState( { 62 | subFolders: subFolders, 63 | expanded: !component.state.expanded 64 | } ); 65 | 66 | component.props.onSelectFolder( component.props.path ); 67 | } ); 68 | } 69 | } 70 | } ); 71 | 72 | module.exports = FolderTree; 73 | -------------------------------------------------------------------------------- /simple-filer/src/js/main.js: -------------------------------------------------------------------------------- 1 | var explorer = require( './explorer.jsx' ); 2 | explorer( '.l-content' ); 3 | -------------------------------------------------------------------------------- /simple-filer/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleFiler", 3 | "version": "1.0.0", 4 | "description": "Example of the simple filer in node-webkit.", 5 | "main": "index.html", 6 | "window": { 7 | "title": "Simple Filer", 8 | "toolbar": true, 9 | "frame": true, 10 | "resizable": true, 11 | "width": 800, 12 | "height": 480, 13 | "position": "mouse" 14 | }, 15 | "platformOverrides": { 16 | "osx": { 17 | "window": { 18 | "toolbar": false 19 | } 20 | }, 21 | "win": { 22 | "window": { 23 | "toolbar": false 24 | } 25 | }, 26 | "linux": { 27 | "window": { 28 | "toolbar": false 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /simple-filer/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/examples-nw/7c6b562784d8bb0b2e018ab6dfc4e50c999d6703/simple-filer/ss.png --------------------------------------------------------------------------------