├── .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 | 
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 |
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 |
15 |
--------------------------------------------------------------------------------
/audio-player/src/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/audio-player/src/icons/play.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/audio-player/src/icons/prev.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/audio-player/src/icons/remove.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/audio-player/src/icons/speaker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | Title |
59 | Artis |
60 | Album |
61 | Duration |
62 |
63 |
64 |
65 | { items }
66 |
67 |
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 |
--------------------------------------------------------------------------------
/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 | Name | Type | Size | Permission | Modified |
52 |
53 |
54 | {items}
55 |
56 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------
/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 | Name | Type | Size | Permission | Modified |
89 |
90 |
91 | {items}
92 |
93 |
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 |
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
--------------------------------------------------------------------------------