├── client ├── libpeerconnection.log ├── .gitattributes ├── app │ ├── .buildignore │ ├── robots.txt │ ├── favicon.ico │ ├── images │ │ ├── LP.png │ │ ├── add.png │ │ ├── back.png │ │ ├── coder.jpg │ │ ├── home.png │ │ ├── user.png │ │ ├── arrow_up.png │ │ ├── listen.png │ │ ├── search.png │ │ ├── yeoman.png │ │ ├── arrow_down.png │ │ ├── ret_album.png │ │ ├── ret_artist.png │ │ └── player_buttons.png │ ├── scripts │ │ ├── filters │ │ │ ├── filters.js │ │ │ ├── module.js │ │ │ └── evenMonthFilter.js │ │ ├── animations │ │ │ ├── animations.js │ │ │ ├── module.js │ │ │ └── blingAnimation.js │ │ ├── services │ │ │ ├── services.js │ │ │ ├── module.js │ │ │ ├── playerService.js │ │ │ └── restService.js │ │ ├── controllers │ │ │ ├── module.js │ │ │ ├── controllers.js │ │ │ ├── userController.js │ │ │ ├── toolbarController.js │ │ │ ├── artistController.js │ │ │ ├── albumController.js │ │ │ ├── tinyPlayerController.js │ │ │ ├── searchController.js │ │ │ ├── homeController.js │ │ │ └── playerController.js │ │ ├── directives │ │ │ ├── module.js │ │ │ ├── directives.js │ │ │ ├── tabDirective.js │ │ │ ├── lyricDirective.js │ │ │ ├── rotateDirective.js │ │ │ └── flexDirective.js │ │ ├── main.js │ │ └── app.js │ ├── views │ │ ├── tabs.html │ │ ├── album.html │ │ ├── toolbar.html │ │ ├── tinyPlayer.html │ │ ├── user.html │ │ ├── artist.html │ │ ├── search.html │ │ ├── home.html │ │ └── player.html │ ├── index.html │ ├── styles │ │ ├── artist.scss │ │ ├── album.scss │ │ ├── user.scss │ │ ├── toolbar.scss │ │ ├── home.scss │ │ ├── tiny.scss │ │ ├── search.scss │ │ ├── main.scss │ │ └── player.scss │ ├── ui-router.html │ ├── 404.html │ └── .htaccess ├── .bowerrc ├── .gitignore ├── .travis.yml ├── test │ ├── runner.html │ ├── spec │ │ └── directives │ │ │ └── rotatedirective.js │ └── .jshintrc ├── .jshintrc ├── .editorconfig ├── bower.json ├── config.rb ├── karma.conf.js ├── karma-e2e.conf.js ├── package.json └── Gruntfile.js ├── server ├── .gitignore ├── data │ ├── artist │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ └── 2.jpg │ ├── cover │ │ ├── 答案.jpg │ │ ├── 夏日的回忆.jpg │ │ ├── davichi合辑.jpg │ │ └── troublemaker.jpg │ ├── music │ │ ├── 乌龟.mp3 │ │ ├── 答案.mp3 │ │ └── default.mp3 │ ├── slider │ │ ├── slider1.jpg │ │ ├── slider2.jpg │ │ └── slider3.jpg │ ├── database │ │ ├── users.json │ │ ├── artists.json │ │ ├── albums.json │ │ └── songs.json │ └── lyric │ │ ├── default.lrc │ │ ├── 我的歌声里.lrc │ │ ├── 答案.lrc │ │ └── 乌龟.lrc ├── package.json ├── routes │ ├── music.js │ ├── artist.js │ ├── album.js │ ├── lyric.js │ ├── cms.js │ ├── search.js │ ├── db.js │ └── user.js └── server.js └── README.md /client/libpeerconnection.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /client/app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /client/app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/favicon.ico -------------------------------------------------------------------------------- /client/app/images/LP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/LP.png -------------------------------------------------------------------------------- /client/app/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/add.png -------------------------------------------------------------------------------- /server/data/artist/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/artist/0.jpg -------------------------------------------------------------------------------- /server/data/artist/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/artist/1.jpg -------------------------------------------------------------------------------- /server/data/artist/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/artist/2.jpg -------------------------------------------------------------------------------- /server/data/cover/答案.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/cover/答案.jpg -------------------------------------------------------------------------------- /server/data/music/乌龟.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/music/乌龟.mp3 -------------------------------------------------------------------------------- /server/data/music/答案.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/music/答案.mp3 -------------------------------------------------------------------------------- /client/app/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/back.png -------------------------------------------------------------------------------- /client/app/images/coder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/coder.jpg -------------------------------------------------------------------------------- /client/app/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/home.png -------------------------------------------------------------------------------- /client/app/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/user.png -------------------------------------------------------------------------------- /server/data/cover/夏日的回忆.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/cover/夏日的回忆.jpg -------------------------------------------------------------------------------- /client/app/images/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/arrow_up.png -------------------------------------------------------------------------------- /client/app/images/listen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/listen.png -------------------------------------------------------------------------------- /client/app/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/search.png -------------------------------------------------------------------------------- /client/app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/yeoman.png -------------------------------------------------------------------------------- /client/app/scripts/filters/filters.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | './evenMonthFilter' 4 | ], function () {}); -------------------------------------------------------------------------------- /server/data/music/default.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/music/default.mp3 -------------------------------------------------------------------------------- /server/data/slider/slider1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/slider/slider1.jpg -------------------------------------------------------------------------------- /server/data/slider/slider2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/slider/slider2.jpg -------------------------------------------------------------------------------- /server/data/slider/slider3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/slider/slider3.jpg -------------------------------------------------------------------------------- /client/app/images/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/arrow_down.png -------------------------------------------------------------------------------- /client/app/images/ret_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/ret_album.png -------------------------------------------------------------------------------- /client/app/images/ret_artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/ret_artist.png -------------------------------------------------------------------------------- /client/app/scripts/animations/animations.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | './blingAnimation' 4 | ], function () {}); -------------------------------------------------------------------------------- /server/data/cover/davichi合辑.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/cover/davichi合辑.jpg -------------------------------------------------------------------------------- /server/data/cover/troublemaker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/server/data/cover/troublemaker.jpg -------------------------------------------------------------------------------- /server/data/database/users.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "stefan", 4 | "songs": [ 5 | 1, 7 6 | ] 7 | }] -------------------------------------------------------------------------------- /server/data/lyric/default.lrc: -------------------------------------------------------------------------------- 1 | [00:00.10]大家好 2 | [00:01.20]这是樊中恺为您制作的Angular音乐盒 3 | [00:04.80]每当这段声音响起时 4 | [00:07:00]代表一段新的音乐开始了 -------------------------------------------------------------------------------- /client/app/images/player_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongkai/angular-music/HEAD/client/app/images/player_buttons.png -------------------------------------------------------------------------------- /client/app/scripts/services/services.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | './playerService', 4 | './restService' 5 | ], function () {}); -------------------------------------------------------------------------------- /client/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | before_script: 6 | - 'npm install -g bower grunt-cli' 7 | - 'bower install' 8 | -------------------------------------------------------------------------------- /client/app/scripts/filters/module.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['angular'], function (angular) { 3 | 'use strict'; 4 | return angular.module('ngMusic.filters', []); 5 | }); -------------------------------------------------------------------------------- /client/app/scripts/services/module.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['angular'], function (angular) { 3 | 'use strict'; 4 | return angular.module('ngMusic.services', []); 5 | }); -------------------------------------------------------------------------------- /client/app/scripts/animations/module.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['angular'], function (angular) { 3 | 'use strict'; 4 | return angular.module('ngMusic.animations', []); 5 | }); -------------------------------------------------------------------------------- /client/app/scripts/controllers/module.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['angular'], function (angular) { 3 | 'use strict'; 4 | return angular.module('ngMusic.controllers', []); 5 | }); -------------------------------------------------------------------------------- /client/app/scripts/directives/module.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['angular'], function (angular) { 3 | 'use strict'; 4 | return angular.module('ngMusic.directives', []); 5 | }); -------------------------------------------------------------------------------- /server/data/database/artists.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 0, 3 | "name": "杨坤" 4 | }, 5 | { 6 | "id": 1, 7 | "name": "Davichi" 8 | }, { 9 | "id": 2, 10 | "name": "Troublemaker" 11 | }] -------------------------------------------------------------------------------- /client/app/scripts/directives/directives.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | './rotateDirective', 4 | './flexDirective', 5 | './lyricDirective', 6 | './tabDirective' 7 | ], function () {}); -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-music-api", 3 | "description": "Stefan Music API", 4 | "version": "0.0.1", 5 | "private": true, 6 | "dependencies": { 7 | "express": "3.x", 8 | "mongodb": "*" 9 | } 10 | } -------------------------------------------------------------------------------- /client/app/views/tabs.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
-------------------------------------------------------------------------------- /client/test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/controllers.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | './homeController', 4 | './playerController', 5 | './searchController', 6 | './toolbarController', 7 | './userController', 8 | './albumController', 9 | './tinyPlayerController', 10 | './artistController' 11 | ], function () {}); -------------------------------------------------------------------------------- /client/app/scripts/controllers/userController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('userController', ['$scope', '$rootScope', 'favs', function($scope, $rootScope, favs) { 5 | $rootScope.title = "个人中心"; 6 | $rootScope.state = "user"; 7 | $scope.favs = favs; 8 | }]); 9 | }); -------------------------------------------------------------------------------- /client/app/scripts/controllers/toolbarController.js: -------------------------------------------------------------------------------- 1 | define(['./module'], function (controllers) { 2 | 'use strict'; 3 | controllers.controller('toolbarController', ['$scope', '$rootScope', '$window', '$timeout', 4 | function($scope, $rootScope, $window, $timeout) { 5 | $scope.back = function() { 6 | $rootScope.slide = 'slide-right'; 7 | $timeout(function() { 8 | $rootScope.slide = 'slide-left'; 9 | }, 250); 10 | $window.history.back(); 11 | }; 12 | }]); 13 | }); -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/artistController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('artistController', ['$scope', '$rootScope', 'artist', 'musics', 'albums', function($scope, $rootScope, artist, musics, albums) { 5 | 6 | $rootScope.title = '歌手-' + artist.name; 7 | $rootScope.state = 'artist'; 8 | 9 | $scope.artist = artist; 10 | $scope.artist.avatar = $rootScope.apiHost + '/data/artist/' + artist.id + '.jpg'; 11 | $scope.musics = musics; 12 | $scope.albums = albums; 13 | 14 | }]); 15 | }); -------------------------------------------------------------------------------- /server/routes/music.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var DB = require('./db'), 3 | db; 4 | 5 | exports.findById = function(req, res) { 6 | db = DB.getDB(); 7 | var id = +req.params.id; 8 | db.collection('songs', function(err, collection) { 9 | collection.findOne({'id': id}, function(err, item) { 10 | res.json(item); 11 | }); 12 | }); 13 | }; 14 | 15 | exports.findAll = function(req, res) { 16 | db = DB.getDB(); 17 | db.collection('songs', function(err, collection) { 18 | collection.find().toArray(function(err, items) { 19 | res.json(items); 20 | }); 21 | }); 22 | }; -------------------------------------------------------------------------------- /server/routes/artist.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var DB = require('./db'), 3 | db; 4 | 5 | exports.findById = function(req, res) { 6 | db = DB.getDB(); 7 | var id = +req.params.id; 8 | db.collection('artists', function(err, collection) { 9 | collection.findOne({'id': id}, function(err, item) { 10 | res.json(item); 11 | }); 12 | }); 13 | }; 14 | 15 | exports.findAll = function(req, res) { 16 | db = DB.getDB(); 17 | db.collection('artists', function(err, collection) { 18 | collection.find().toArray(function(err, items) { 19 | res.json(items); 20 | }); 21 | }); 22 | }; -------------------------------------------------------------------------------- /client/test/spec/directives/rotatedirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: rotateDirective', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('ugMusicApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element(''); 17 | element = $compile(element)(scope); 18 | expect(element.text()).toBe('this is the rotateDirective directive'); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /client/app/scripts/filters/evenMonthFilter.js: -------------------------------------------------------------------------------- 1 | define(['./module'], function (filters) { 2 | 'use strict'; 3 | filters.filter('evenMonth', function() { 4 | return function(input, attr, specifyYear) { 5 | 6 | var output = [], year, month, publish; 7 | 8 | angular.forEach(input, function(item) { 9 | 10 | if(publish = item[attr]) { 11 | year = parseInt(publish / 10000); 12 | month = parseInt((publish % 10000) / 100); 13 | console.info(year, month); 14 | if(!specifyYear || specifyYear && specifyYear == year) { 15 | month % 2 == 0 && output.push(item); 16 | } 17 | } 18 | }); 19 | 20 | return output; 21 | } 22 | }); 23 | }); -------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ug-music", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "1.2.6", 6 | "json3": "~3.2.6", 7 | "es5-shim": "~2.1.0", 8 | "requirejs": "2.1.10", 9 | "requirejs-domready": "2.0.1", 10 | "angular-route": "~1.2.9", 11 | "angular-touch": "~1.2.9", 12 | "angular-gestures": "~0.2.1", 13 | "angular-resource": "~1.2.10", 14 | "angular-animate": "~1.2.11", 15 | "jquery": "~2.1.0", 16 | "jqueryui": "~1.10.3", 17 | "angular-sanitize": "~1.2.16" 18 | }, 19 | "devDependencies": { 20 | "angular-mocks": "1.2.6", 21 | "angular-scenario": "1.2.6", 22 | "angular-route": "~1.2.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/app/views/album.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{title}} 4 |
5 | 21 |
22 | -------------------------------------------------------------------------------- /client/app/views/toolbar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 返回 5 | 6 | 7 |
{{$parent.title}}
8 |
9 | 10 | 个人 11 | 搜索 12 | 首页 13 | 14 |
15 |
-------------------------------------------------------------------------------- /client/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /server/data/database/albums.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 0, 3 | "name": "答案", 4 | "artist": 0, 5 | "image": "/data/cover/davichi合辑.jpg", 6 | "desc": "杨坤/郭采洁-答案", 7 | "songs": [ 8 | 0 9 | ] 10 | }, { 11 | "id": 1, 12 | "name": "Davichi经典合辑", 13 | "artist": 1, 14 | "image": "/data/cover/davichi合辑.jpg", 15 | "desc": "一路走来的经典歌曲合辑。", 16 | "songs": [ 17 | 1, 2, 3, 4, 5, 6, 7, 8 18 | ] 19 | 20 | }, { 21 | "name": "夏日的回忆", 22 | "artist": 1, 23 | "image": "/data/cover/夏日的回忆.jpg", 24 | "desc": "我抱着你的盒子,是因为今天格外想你。", 25 | "songs": [ 26 | 1, 2, 3, 4, 5, 6, 7, 8 27 | ] 28 | }, { 29 | "name": "Troublemaker", 30 | "artist": 2, 31 | "image": "/data/cover/troublemaker.jpg", 32 | "desc": "我抱着你的盒子,是因为今天格外想你。", 33 | "songs": [ 34 | 9 35 | ] 36 | }] -------------------------------------------------------------------------------- /client/app/scripts/controllers/albumController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('albumController', ['$scope', '$rootScope', '$routeParams', '$timeout', 'playerService', 'albumAPI', function($scope, $rootScope, $routeParams, $timeout, playerService, albumAPI) { 5 | $rootScope.title = "专辑"; 6 | $rootScope.state = "album"; 7 | 8 | albumAPI.fetchById($routeParams.id).then(function(ret) { 9 | $scope.songs = ret.songs; 10 | $scope.title = ret.name; 11 | }); 12 | 13 | $scope.playAll = function() { 14 | $location.path('#/player/'); 15 | }; 16 | 17 | $scope.add = function(music) { 18 | music.addClass = 'bling'; 19 | playerService.add(music); 20 | $timeout(function() { 21 | music.addClass = ''; 22 | }, 1000); 23 | }; 24 | }]); 25 | }); -------------------------------------------------------------------------------- /client/app/views/tinyPlayer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
-------------------------------------------------------------------------------- /server/routes/album.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var DB = require('./db'), 3 | db; 4 | 5 | exports.findById = function(req, res) { 6 | db = DB.getDB(); 7 | var id = +req.params.id; 8 | db.collection('albums', function(err, collection) { 9 | collection.findOne({'id': id}, function(err, item) { 10 | 11 | db.collection('songs', function(err, collection) { 12 | collection.find({id: {$in: item.songs}}).toArray(function(err, ret) { 13 | 14 | item.songs = ret; 15 | 16 | res.json(item); 17 | 18 | }); 19 | }); 20 | 21 | }); 22 | }); 23 | }; 24 | 25 | exports.findAll = function(req, res) { 26 | db = DB.getDB(); 27 | db.collection('albums', function(err, collection) { 28 | collection.find().toArray(function(err, items) { 29 | res.json(items); 30 | }); 31 | }); 32 | 33 | }; -------------------------------------------------------------------------------- /server/routes/lyric.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var fs = require('fs'), 3 | path = require('path'); 4 | 5 | var lyricPath = path.join(__dirname, '../data/lyric'); 6 | 7 | exports.getLyric = function(req, res) { 8 | 9 | var file = req.params.file; 10 | fs.readFile(lyricPath + '/' + file, 'utf8', function(err, data) { 11 | if (err) throw err; 12 | 13 | var lines = data.split('\n'), 14 | jsonRegex = /\[(.*?)\](.*)$/, 15 | ret = [], matchResult; 16 | 17 | lines.forEach(function(line) { 18 | matchResult = line.match(jsonRegex); 19 | matchResult && 20 | matchResult[1] && 21 | matchResult[2] && 22 | ret.push({ 23 | time: parseTime(matchResult[1]), 24 | content: matchResult[2] 25 | }); 26 | }); 27 | 28 | res.json(ret); 29 | }); 30 | }; 31 | 32 | function parseTime(lrcTime) { 33 | var ret = lrcTime.split(':'); 34 | return parseInt(ret[0]) * 60 + parseFloat(ret[1]) + ''; 35 | } -------------------------------------------------------------------------------- /client/app/scripts/controllers/tinyPlayerController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('tinyPlayerController', ['$scope', '$rootScope', '$window', '$interval', '$http', 'playerService', 5 | function ($scope, $rootScope, $window, $interval, $http, playerService) { 6 | 7 | $scope.player = { 8 | 9 | playing: true, 10 | 11 | play: function() { 12 | if($rootScope.playEmpty) return; 13 | 14 | if(this.playing) { 15 | playerService.pause(); 16 | } 17 | else { 18 | playerService.play(); 19 | } 20 | 21 | this.playing = !this.playing; 22 | 23 | }, 24 | next: function() { 25 | if($rootScope.playEmpty) return; 26 | var nextSong = playerService.next(); 27 | }, 28 | previous: function() { 29 | if($rootScope.playEmpty) return; 30 | var previousSong = playerService.previous(); 31 | } 32 | }; 33 | 34 | }]); 35 | }); -------------------------------------------------------------------------------- /client/config.rb: -------------------------------------------------------------------------------- 1 | # Require any additional compass plugins here. 2 | 3 | # Set this to the root of your project when deployed: 4 | http_path = "/" 5 | css_dir = ".tmp/styles" 6 | sass_dir = "app/styles" 7 | images_dir = "app/images" 8 | javascripts_dir = "app/scripts" 9 | 10 | # You can select your preferred output style here (can be overridden via the command line): 11 | # output_style = :expanded or :nested or :compact or :compressed 12 | 13 | # To enable relative paths to assets via compass helper functions. Uncomment: 14 | # relative_assets = true 15 | 16 | # To disable debugging comments that display the original location of your selectors. Uncomment: 17 | # line_comments = false 18 | 19 | 20 | # If you prefer the indented syntax, you might want to regenerate this 21 | # project again passing --syntax sass, or you can uncomment this: 22 | # preferred_syntax = :sass 23 | # and then run: 24 | # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass 25 | -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Music 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /client/app/views/user.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 | 8 | 9 | 10 | 11 |

12 | 我是一个程序员,我要用AngularJS开发一款名叫“Angular音乐盒”的Web应用。 13 |

14 |
15 |
16 |
17 |
我收藏的歌曲
18 | 32 |
33 |
-------------------------------------------------------------------------------- /server/data/lyric/我的歌声里.lrc: -------------------------------------------------------------------------------- 1 | 2 | [00:00.50]我的歌声里 3 | [00:02.62]词曲:曲婉婷 4 | [00:03.62]演唱:曲婉婷 5 | [00:09.03] 6 | [00:14.13]没有一点点防备 7 | [00:16.68]也没有一丝顾虑 8 | [00:18.42]你就这样出现在我的世界里 9 | [00:22.89]带给我惊喜 情不自已 10 | [00:27.71] 11 | [00:28.36]可是你偏又这样 12 | [00:30.77]在我不知不觉中 悄悄的消失 13 | [00:34.63]从我的世界里 没有音讯 14 | [00:38.21]剩下的只是回忆 15 | [00:42.24]你存在 我深深的脑海里 16 | [00:48.62]我的梦里 我的心里 我的歌声里 17 | [00:54.91] 18 | [00:56.08]你存在 我深深的脑海里 19 | [01:02.56]我的梦里 我的心里 我的歌声里 20 | [01:09.30] 21 | [01:10.83]还记得我们曾经 22 | [01:12.41]肩并肩一起走过 那段繁华巷口 23 | [01:16.55]尽管你我是陌生人 是过路人 24 | [01:20.37]但彼此还是感觉到了对方的 25 | [01:24.81]一个眼神 一个心跳 26 | [01:27.72]一种意想不到的快乐 27 | [01:33.79]好像是一场梦境 命中注定 28 | [01:41.64]你存在 我深深的脑海里 29 | [01:47.90]我的梦里 我的心里 我的歌声里 30 | [01:55.30]你存在 我深深的脑海里 31 | [02:01.80]我的梦里 我的心里 我的歌声里 32 | [02:08.65] 33 | [02:09.46]世界之大为何我们相遇 34 | [02:15.68]难道是缘分 难道是天意 35 | [02:23.64] 36 | [02:26.22]你存在 我深深的脑海里 37 | [02:33.19]我的梦里 我的心里 我的歌声里 38 | [02:40.83]你存在 我深深的脑海里 39 | [02:47.14]我的梦里 我的心里 我的歌声里 40 | [02:54.04] 41 | [02:54.92]你存在 我深深的脑海里 42 | [03:01.10]我的梦里 我的心里 我的歌声里 43 | [03:15.93] 44 | -------------------------------------------------------------------------------- /client/app/scripts/animations/blingAnimation.js: -------------------------------------------------------------------------------- 1 | define(['./module'], function (animations) { 2 | 'use strict'; 3 | animations.animation('.bling', function() { 4 | return { 5 | addClass : function(element, className, done) { 6 | 7 | element.css({ 8 | //此处省略多浏览器支持 9 | '-webkit-animation': 'bling-keyframes .5s linear infinite' 10 | }); 11 | //动画执行完成调用done参数 12 | jQuery(element).animate({ 13 | color: 'red' 14 | }, 500, 'linear', done); 15 | 16 | //可选,动画完毕或者取消时会被调用 17 | return function(isCancelled) { 18 | if(isCancelled) { 19 | jQuery(element).stop(); 20 | } 21 | }; 22 | }, 23 | 24 | removeClass : function(element, className, done) { 25 | 26 | element.css({ 27 | '-webkit-animation': '' 28 | }); 29 | jQuery(element).animate({ 30 | color: 'black' 31 | }, 500, 'linear', done); 32 | 33 | //可选,动画完毕或者取消时会被调用 34 | return function(isCancelled) { 35 | if(isCancelled) { 36 | jQuery(element).stop(); 37 | } 38 | }; 39 | } 40 | }; 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /client/app/styles/artist.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | .artist.container { 3 | 4 | .info { 5 | 6 | width: 100%; 7 | 8 | .avatar { 9 | width: 60px; 10 | height: 60px; 11 | margin: 5px auto; 12 | display: block; 13 | } 14 | } 15 | 16 | ul.musics, ul.albums { 17 | 18 | li { 19 | 20 | a { 21 | text-decoration: none; 22 | color: black; 23 | } 24 | 25 | &:first-child { 26 | border-top: none; 27 | } 28 | 29 | height: 40px; 30 | border-top: 1px solid #fff; 31 | background: #ddd; 32 | border-bottom: 1px solid #9a9a9a; 33 | 34 | .title { 35 | padding-left: 20px; 36 | font-size: 18px; 37 | height: 40px; 38 | line-height: 40px; 39 | } 40 | 41 | .listen { 42 | background: url(../images/listen.png) center center no-repeat; 43 | @include background-size(20px 20px); 44 | } 45 | 46 | .album { 47 | background: url(../images/ret_album.png) center center no-repeat; 48 | @include background-size(20px 20px); 49 | } 50 | 51 | .add { 52 | background: url(../images/add.png) center center no-repeat; 53 | @include background-size(20px 20px); 54 | cursor: pointer; 55 | } 56 | } 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular音乐盒 2 | 3 | #### 一个基于AngularJS的移动WebAPP项目 4 | 5 | --- 6 | 7 | ## 客户端 8 | 9 | 客户端的目录结构使用Yeoman生成,位于`client`文件夹内部。 10 | 11 | 为了使得调试/开发环境可用,请按照如下步骤确保开发环境完备正常: 12 | 13 | * 安装Node.js、bower、Compass 14 | * 进入`client`目录执行如下命令,通过NPM安装依赖模块(主要是Grunt及其插件): 15 | 16 | $ npm install 17 | 18 | * 进入`client`目录执行如下命令安装依赖的JavaScript库: 19 | 20 | $ bower install 21 | 22 | * 执行`$ grunt server`开启浏览器,启动调试(请阅读[FAQ](#如何在chrome中调试本项目)来确保调试正常)。 23 | 24 | ## 服务端 25 | 26 | 本项目服务端代码放置于`server`文件夹内,为发布后的客户端提供RESTFUL风格的API接口。本应用使用`NodeJS + MongoDB`来实现请求处理和数据存储。请按照如下步骤确保服务运行: 27 | 28 | * 安装Node.js和MongoDB 29 | * 进入`server`文件夹执行如下命令,通过NPM安装依赖模块: 30 | 31 | $ npm install 32 | 33 | * 执行如下命令开启服务: 34 | 35 | $ node server 36 | 37 | * 访问`http://localhost:1987`来访问发布后的应用,如果需要改变端口号,请修改`server/server.js`文件。 38 | 39 | ## FAQ 40 | 41 | ### 如何在Chrome中调试本项目 42 | 43 | 本项目调试使用的是`grunt-contrib-connect`插件来提供静态服务器,为了能够访问服务端资源,请通过如下命令关闭Chrome的跨域安全限制: 44 | 45 | 针对Mac: 46 | 47 | $ open -a Google\ Chrome --args --disable-web-security 48 | 49 | 针对Linux: 50 | 51 | $ google-chrome --disable-web-security 52 | 53 | 针对Windows: 54 | 55 | chrome.exe --disable-web-security 56 | 57 | > 如果希望开启本地文件访问,可以使用`--allow-file-access-from-files`参数。 -------------------------------------------------------------------------------- /server/data/lyric/答案.lrc: -------------------------------------------------------------------------------- 1 | 2 | [00:02.00]答案 3 | [00:04.00]词:梁芒 4 | [00:06.00]曲:杨坤 5 | [00:08.00]演唱:杨坤,郭采洁 6 | [00:10.00] 7 | [00:13.30](女)有个简单的问题 8 | [00:16.52]什么是爱情 9 | [00:19.57]它是否是一种味道 还是引力 10 | [00:26.28](男)从我初恋那天起 先是甜蜜 11 | [00:32.59]然后紧接就会有 风雨 12 | [00:38.75](合)爱就像 蓝天白云 13 | [00:40.89]晴空万里 突然暴风雨 14 | [00:45.83]无处躲避 总是让人 始料不及 15 | [00:51.60]人就像 患重感冒 16 | [00:53.90]打着喷嚏 发烧要休息 17 | [00:58.76]冷热交替 欢喜犹豫 乐此不疲 18 | [01:07.63] 19 | [01:11.65](女)叫人头晕的问题 20 | [01:15.07]什么叫爱情 21 | [01:18.05]它是那么的真实 又很可疑 22 | [01:24.60](男)研究过许多誓言 海枯石烂 23 | [01:31.15]发觉越想要解释 越乱 24 | [01:37.08](女)所以说 永远多长 25 | [01:39.33]永远短暂 永远很遗憾 26 | [01:44.15]每个人有 每个人不同的体验 27 | [01:50.12](合)那滋味 时而在飞 28 | [01:52.25]时而下坠 时而又落泪 29 | [01:57.19]动人电影 自己体会 别嫌票太贵 30 | [02:04.97](男)比如美酒和咖啡 都是水 31 | [02:10.89](女) 可一个让你醒 一个让你醉 32 | [02:15.91] 33 | [02:17.72]那么感情 34 | [02:19.82](合)能否以此类推 35 | [02:22.76]有的很平淡 有的撕心裂肺 36 | [02:32.12]所以说 永远多长 37 | [02:34.48]永远短暂 永远很缓慢 38 | [02:39.29]每个人有 每个人不同的计算 39 | [02:45.00]神秘的 诺言誓言 40 | [02:47.36]甚至谎言 自己去领悟 41 | [02:52.21]也许多年 也许瞬间 42 | [02:55.53]你自有答案 43 | [02:57.94]所以说 永远多长 44 | [03:00.28]永远短暂 永远很缓慢 45 | [03:05.15]每个人有 每个人不同的计算 46 | [03:10.88]神秘的 诺言誓言 47 | [03:13.38]甚至谎言 自己去领悟 48 | [03:18.11]也许多年 也许瞬间 49 | [03:21.47]你自有答案 50 | [03:44.85] 51 | -------------------------------------------------------------------------------- /client/app/views/artist.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | 22 | 23 | 24 | 37 | 38 | 39 |
-------------------------------------------------------------------------------- /client/app/scripts/directives/tabDirective.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (directives) { 3 | 'use strict'; 4 | directives.directive('tabs', function () { 5 | return { 6 | restrict: 'E', 7 | transclude: true, 8 | templateUrl: '../../views/tabs.html', 9 | replace: true, 10 | scope: {}, 11 | controller: ['$scope', function($scope) { 12 | $scope.tabs = []; 13 | 14 | $scope.select = function(tab) { 15 | angular.forEach($scope.tabs, function(tab) { 16 | tab.selected = false; 17 | }); 18 | tab.selected = true; 19 | }; 20 | 21 | this.add = function(tab) { 22 | if ($scope.tabs.length == 0) { 23 | $scope.select(tab); 24 | } 25 | $scope.tabs.push(tab); 26 | }; 27 | }] 28 | }; 29 | }).directive('tab', function() { 30 | return { 31 | require: '^tabs', 32 | restrict: 'E', 33 | replace: true, 34 | transclude: true, 35 | template: '
', 36 | scope: { 37 | title: '@' 38 | }, 39 | link: function(scope, element, attrs, tabsCtrl) { 40 | scope.selected = false; 41 | tabsCtrl.add(scope); 42 | } 43 | }; 44 | }); 45 | }); -------------------------------------------------------------------------------- /client/app/styles/album.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | .album.container { 3 | 4 | .album-title { 5 | background: #555; 6 | color: white; 7 | text-align: center; 8 | font-size: 16px; 9 | line-height: 25px; 10 | height: 25px; 11 | 12 | button { 13 | border: 2px solid #fff; 14 | border-radius: 2px; 15 | height: 19px; 16 | float: right; 17 | margin: 3px 10px; 18 | color: white; 19 | padding: 0 5px; 20 | } 21 | } 22 | 23 | ul.songs { 24 | 25 | li { 26 | 27 | a { 28 | text-decoration: none; 29 | color: black; 30 | } 31 | 32 | &:first-child { 33 | border-top: none; 34 | } 35 | 36 | height: 40px; 37 | border-top: 1px solid #fff; 38 | background: #ddd; 39 | border-bottom: 1px solid #9a9a9a; 40 | 41 | .name { 42 | padding-left: 20px; 43 | font-size: 18px; 44 | height: 40px; 45 | line-height: 40px; 46 | } 47 | 48 | .listen { 49 | background: url(../images/listen.png) center center no-repeat; 50 | @include background-size(20px 20px); 51 | } 52 | 53 | .add { 54 | text-align: center; 55 | line-height: 40px; 56 | font-size: 26px; 57 | font-weight: 900; 58 | cursor: pointer; 59 | } 60 | } 61 | 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /server/routes/cms.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var DB = require('./db'), 3 | db; 4 | 5 | exports.findSliders = function(req, res) { 6 | res.json([ 7 | { 8 | img: 'data/slider/slider1.jpg', 9 | id: 1 10 | }, 11 | { 12 | img: 'data/slider/slider2.jpg', 13 | id: 2 14 | }, 15 | { 16 | img: 'data/slider/slider3.jpg', 17 | id: 3 18 | } 19 | ]); 20 | }; 21 | 22 | exports.findHots = exports.findNews = function(req, res) { 23 | db = DB.getDB(); 24 | var artistIdArr = []; 25 | db.collection('songs', function(err, collection) { 26 | collection.find().toArray(function(err, items) { 27 | 28 | db.collection('artists', function(err, collection) { 29 | items.forEach(function(item) { 30 | artistIdArr.push(item.artist); 31 | }); 32 | 33 | collection.find({id: {$in: artistIdArr}}).toArray(function(err, ret) { 34 | 35 | items.forEach(function(item, index) { 36 | ret.forEach(function(retItem) { 37 | if(retItem['id'] == item.artist) { 38 | item.artistName = retItem['name']; 39 | } 40 | }) 41 | }); 42 | 43 | res.json(items); 44 | }); 45 | }); 46 | }); 47 | }); 48 | }; -------------------------------------------------------------------------------- /server/routes/search.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | var DB = require('./db'), 3 | db; 4 | 5 | exports.search = function(req, res) { 6 | db = DB.getDB(); 7 | var keyword = req.query.key, 8 | type = +req.query.type, 9 | artist = +req.query.artist, 10 | o = {}, regexp; 11 | 12 | if(keyword) regexp = new RegExp('.*' + keyword + '.*'); 13 | 14 | if(artist) o.artist = artist; 15 | 16 | if(type == 1) { 17 | db.collection('artists', function(err, collection) { 18 | 19 | if(keyword) o.name = regexp; 20 | 21 | collection.find(o).toArray(function(err, items) { 22 | res.json(items); 23 | }); 24 | }); 25 | } 26 | else if(type == 2) { 27 | db.collection('albums', function(err, collection) { 28 | 29 | if(keyword) o.name = regexp; 30 | 31 | collection.find(o).toArray(function(err, items) { 32 | res.json(items); 33 | }); 34 | }); 35 | } 36 | else { 37 | db.collection('songs', function(err, collection) { 38 | 39 | if(keyword) o.title = regexp; 40 | 41 | collection.find(o).toArray(function(err, items) { 42 | res.json(items); 43 | }); 44 | }); 45 | } 46 | 47 | }; -------------------------------------------------------------------------------- /client/app/styles/user.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | .user { 3 | 4 | .avatar { 5 | width: 55px; 6 | height: 65px; 7 | margin: 10px; 8 | } 9 | 10 | .desc { 11 | padding: 10px; 12 | word-break: break-all; 13 | } 14 | 15 | .fav { 16 | .fav-title { 17 | background: #555; 18 | color: white; 19 | text-align: center; 20 | font-size: 16px; 21 | line-height: 25px; 22 | height: 25px; 23 | } 24 | 25 | ul.favs { 26 | 27 | li { 28 | &:first-child { 29 | border-top: none; 30 | } 31 | 32 | height: 40px; 33 | border-top: 1px solid #fff; 34 | background: #ddd; 35 | border-bottom: 1px solid #9a9a9a; 36 | 37 | .title { 38 | padding-left: 5px; 39 | font-size: 18px; 40 | height: 26px; 41 | line-height: 26px; 42 | } 43 | 44 | .artist { 45 | padding-left: 5px; 46 | color: #999; 47 | font-size: 8px; 48 | height: 14px; 49 | line-height: 14px; 50 | } 51 | 52 | .add { 53 | background: url(../images/add.png) center center no-repeat; 54 | @include background-size(20px 20px); 55 | cursor: pointer; 56 | } 57 | 58 | .listen { 59 | background: url(../images/listen.png) center center no-repeat; 60 | @include background-size(20px 20px); 61 | } 62 | } 63 | 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /client/app/styles/toolbar.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | 3 | .toolbar { 4 | position: fixed; 5 | z-index: 100; 6 | top: 0; 7 | @include box-shadow(#555 0px 0px 5px 0px); 8 | width: 100%; 9 | height: 40px; 10 | @include background-image(linear-gradient(#666666, #333333)); 11 | border-bottom: 1px solid #222; 12 | color: #fff; 13 | 14 | .user, .search, .home, .back { 15 | text-indent: -9999px; 16 | width: 26px; 17 | height: 40px; 18 | margin: 0 5px; 19 | cursor: pointer; 20 | float: right; 21 | } 22 | 23 | .back { 24 | background: url('/images/back.png') center center no-repeat; 25 | @include background-size(20px 20px); 26 | float: left; 27 | } 28 | 29 | .search { 30 | background: url('/images/search.png') center center no-repeat; 31 | @include background-size(20px 20px); 32 | } 33 | 34 | .home { 35 | background: url('/images/home.png') center center no-repeat; 36 | @include background-size(20px 20px); 37 | } 38 | 39 | .user { 40 | background: url('/images/user.png') center center no-repeat; 41 | @include background-size(20px 20px); 42 | } 43 | 44 | .back:hover, .user:hover, .search:hover, .home:hover { 45 | opacity: .8; 46 | } 47 | 48 | .title-box { 49 | height: 40px; 50 | line-height: 40px; 51 | font-size: 16px; 52 | color: #fff; 53 | text-align: center; 54 | text-overflow: ellipsis; 55 | white-space:nowrap; 56 | } 57 | } -------------------------------------------------------------------------------- /client/app/scripts/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | //设置项目所依赖的库文件别名 3 | paths: { 4 | // 'jQuery': '../bower_components/jquery/jquery', 5 | // 'jQueryUI': '../bower_components/jqueryui/ui/jquery-ui', 6 | 'angular': '../bower_components/angular/angular', 7 | 'angularRoute': '../bower_components/angular-route/angular-route', 8 | 'angularTouch': '../bower_components/angular-touch/angular-touch', 9 | 'angularGestures': '../bower_components/angular-gestures/gestures', 10 | 'angularResource': '../bower_components/angular-resource/angular-resource', 11 | 'angularAnimate': '../bower_components/angular-animate/angular-animate', 12 | 'domReady': '../bower_components/requirejs-domready/domReady' 13 | }, 14 | //把angualrjs当做AMD模块来使用 15 | shim: { 16 | 'angular': { 17 | exports: 'angular' 18 | }, 19 | 'angularRoute': { 20 | deps: ['angular'] 21 | }, 22 | 'angularTouch': { 23 | deps: ['angular'] 24 | }, 25 | 'angularGestures': { 26 | deps: ['angular'] 27 | }, 28 | 'angularResource': { 29 | deps: ['angular'] 30 | }, 31 | 'angularAnimate': { 32 | deps: ['angular'] 33 | } 34 | } 35 | }); 36 | 37 | require([ 38 | 'angular', 39 | 'domReady', 40 | // 'jQuery', 41 | // 'jQueryUI', 42 | 'angularRoute', 43 | 'angularTouch', 44 | 'angularGestures', 45 | 'angularResource', 46 | 'angularAnimate', 47 | 'app' 48 | ], 49 | function (angular, domReady) { 50 | 'use strict'; 51 | 52 | domReady(function() { 53 | angular.bootstrap(document, ['ngMusic']); 54 | }); 55 | }); -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'app/bower_components/angular/angular.js', 15 | 'app/bower_components/angular-mocks/angular-mocks.js', 16 | 'app/scripts/*.js', 17 | 'app/scripts/**/*.js', 18 | 'test/mock/**/*.js', 19 | 'test/spec/**/*.js' 20 | ], 21 | 22 | // list of files / patterns to exclude 23 | exclude: [], 24 | 25 | // web server port 26 | port: 8080, 27 | 28 | // level of logging 29 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 30 | logLevel: config.LOG_INFO, 31 | 32 | 33 | // enable / disable watching file and executing tests whenever any file changes 34 | autoWatch: false, 35 | 36 | 37 | // Start these browsers, currently available: 38 | // - Chrome 39 | // - ChromeCanary 40 | // - Firefox 41 | // - Opera 42 | // - Safari (only Mac) 43 | // - PhantomJS 44 | // - IE (only Windows) 45 | browsers: ['Chrome'], 46 | 47 | 48 | // Continuous Integration mode 49 | // if true, it capture browsers, run tests and exit 50 | singleRun: false 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /client/app/scripts/directives/lyricDirective.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (directives) { 3 | 'use strict'; 4 | directives.directive('lyric', function () { 5 | return { 6 | template: '
', 7 | restrict: 'EA', 8 | replace: true, 9 | scope: { 10 | source: '@', 11 | time: '@' 12 | }, 13 | link: function (scope, element, attrs) { 14 | 15 | var src = null; 16 | 17 | scope.$watch("source", function(value) { 18 | var lis = []; 19 | src = JSON.parse(value); 20 | src.forEach(function(v) { 21 | lis.push('
  • ' + v.content + '
  • '); 22 | }); 23 | element.html('') 24 | }); 25 | 26 | scope.$watch("time", function(value) { 27 | var t, index = 0, ulEl, 28 | lineHeight, light = false; 29 | 30 | if(src) { 31 | ulEl = element.children().eq(0); 32 | lineHeight = ulEl.children().eq(0)[0].clientHeight; 33 | src.forEach(function(v) { 34 | if(+v.time > +value && !light) { 35 | ulEl.css('top', 36 | -lineHeight * index + element[0].clientHeight / 2 + 'px'); 37 | ulEl.children().eq(index - 1).css({ 38 | background: '#555', 39 | color: '#fff' 40 | }); 41 | light = true; 42 | } 43 | else { 44 | ulEl.children().eq(index).css({ 45 | background: 'transparent', 46 | color: '#000' 47 | }); 48 | } 49 | index++; 50 | }); 51 | } 52 | 53 | }); 54 | } 55 | }; 56 | }); 57 | }); -------------------------------------------------------------------------------- /client/app/styles/home.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | 3 | .home { 4 | 5 | .carousel { 6 | 7 | overflow: hidden; 8 | 9 | background: #aaa; 10 | 11 | border-bottom: 1px solid #9a9a9a; 12 | 13 | ul { 14 | &.animate { 15 | @include transition-property(all); 16 | @include transition-duration(.3s); 17 | @include transition-timing-function(ease-in); 18 | } 19 | 20 | li { 21 | 22 | list-style: none; 23 | display: table-cell; 24 | 25 | img { 26 | width: 275px; 27 | height: 183px; 28 | display: block; 29 | margin: 0 auto; 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | } 37 | 38 | ul.hots, ul.news { 39 | 40 | li { 41 | 42 | a { 43 | text-decoration: none; 44 | color: black; 45 | } 46 | 47 | &:first-child { 48 | border-top: none; 49 | } 50 | 51 | height: 40px; 52 | border-top: 1px solid #fff; 53 | background: #ddd; 54 | border-bottom: 1px solid #9a9a9a; 55 | 56 | .title { 57 | padding-left: 5px; 58 | font-size: 18px; 59 | height: 26px; 60 | line-height: 26px; 61 | } 62 | 63 | .artist { 64 | padding-left: 5px; 65 | color: #999; 66 | font-size: 8px; 67 | height: 14px; 68 | line-height: 14px; 69 | } 70 | 71 | .listen { 72 | background: url(../images/listen.png) center center no-repeat; 73 | @include background-size(20px 20px); 74 | } 75 | 76 | .add { 77 | text-align: center; 78 | line-height: 40px; 79 | font-size: 26px; 80 | font-weight: 900; 81 | cursor: pointer; 82 | } 83 | } 84 | 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /client/karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /client/app/scripts/directives/rotateDirective.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (directives) { 3 | 'use strict'; 4 | directives.directive('rotate', function () { 5 | return { 6 | template: '
    ', 7 | restrict: 'EA', 8 | transclude: true, 9 | replace: true, 10 | scope: { 11 | // degrees: '@', 12 | // speed: '@' 13 | rotateDegrees: '@degrees', 14 | rotateSpeed: '=speed' 15 | 16 | }, 17 | link: function (scope, element, attrs) { 18 | //自动旋转 19 | //speed: 0-停止, 1-slow, 2-normal, 3-fast 20 | scope.$watch('rotateSpeed', function(value) { 21 | element.removeClass('rotate-1') 22 | .removeClass('rotate-2') 23 | .removeClass('rotate-3'); 24 | 25 | if(value > 0) { 26 | element.addClass('rotate-' + value); 27 | } 28 | }); 29 | 30 | //受控制的旋转 31 | scope.$watch('rotateDegrees', function(value) { 32 | element.css({ 33 | '-moz-transform': 'rotate(' + value + 'deg)', 34 | '-webkit-transform': 'rotate(' + value + 'deg)', 35 | '-o-transform': 'rotate(' + value + 'deg)', 36 | '-ms-transform': 'rotate(' + value + 'deg)' 37 | }); 38 | }); 39 | 40 | //使用$observe方法来监控 41 | // attrs.$observe('degrees', function(value) { 42 | // element.css({ 43 | // '-moz-transform': 'rotate(' + value + 'deg)', 44 | // '-webkit-transform': 'rotate(' + value + 'deg)', 45 | // '-o-transform': 'rotate(' + value + 'deg)', 46 | // '-ms-transform': 'rotate(' + value + 'deg)' 47 | // }); 48 | // }); 49 | 50 | } 51 | }; 52 | }); 53 | }); -------------------------------------------------------------------------------- /client/app/scripts/controllers/searchController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('searchController', ['$scope', '$rootScope', '$http', '$location', 'playerService', 'orderByFilter', 5 | function($scope, $rootScope, $http, $location, playerService, orderByFilter) { 6 | 7 | $rootScope.title = '搜索'; 8 | $rootScope.state = 'search'; 9 | 10 | $scope.select = { 11 | items: ['歌曲', '歌手', '专辑'], 12 | selected: '歌曲', 13 | expand: false 14 | }; 15 | 16 | $scope.keyword = ''; 17 | 18 | $scope.query = function() { 19 | if($scope.keyword) { 20 | $http.get($rootScope.apiHost + '/search?key=' + $scope.keyword + '&type=' + 21 | $scope.select.items.indexOf($scope.select.selected)).success(function(rets) { 22 | $scope.rets = rets; 23 | }); 24 | } 25 | }; 26 | 27 | $scope.add = function(music) { 28 | playerService.add(music); 29 | }; 30 | 31 | $scope.goResult = function(ret) { 32 | if($scope.select.selected == '歌曲') { 33 | $location.path('/player/' + ret.id); 34 | } 35 | else if($scope.select.selected == '歌手') { 36 | $location.path('/artist/' + ret.id); 37 | } 38 | else if($scope.select.selected == '专辑') { 39 | $location.path('/album/' + ret.id); 40 | } 41 | } 42 | 43 | // $scope.$watch('select.selected', function(val) { 44 | // if(val == '歌曲') { 45 | // $scope.rets = orderByFilter($scope.rets, '-publish'); 46 | // } 47 | // else if(val == '专辑') { 48 | // $scope.rets = orderByFilter($scope.rets, '+publish'); 49 | // } 50 | // }); 51 | 52 | }]); 53 | }); -------------------------------------------------------------------------------- /server/data/lyric/乌龟.lrc: -------------------------------------------------------------------------------- 1 | [ti:乌龟] 2 | [ar:Davichi] 3 | [al:] 4 | 5 | [00:00.69]다비치(Davichi)- 거북이(乌龟) 6 | [00:16.96] 7 | [00:19.19]거북아 그 속도론 멀리 못 도망가 8 | [00:23.26]게다가 그 길은 더 멀고 험하잖아 9 | [00:27.33]상처가 아물고 다 나으면 떠나가 10 | [00:31.70]진심이야 그럼 그 때 보내 줄 테니까 11 | [00:35.49]숨지마 차라리 내 맘을 훔치지마 12 | [00:39.42]거짓말 느리고 느린 너의 걸음마 13 | [00:43.53]내 가슴 깊이 하는 말 14 | [00:47.54]내게로 와요 15 | [00:50.49] 16 | [00:50.70]마음을 둘 곳도 없고 17 | [00:53.01]더 갈 곳도 없는 18 | [00:55.10]슬픈 거북이 한 마리 19 | [00:58.74]상처가 많아 너 혼자서 20 | [01:02.95]매일 외롭게 숨는 거니 21 | [01:06.93]너를 지킬 수 없고 22 | [01:09.29]더 사랑도 없는 23 | [01:11.34]내 가슴 아픈 이야기 24 | [01:15.66]조금 늦어도 좋아 25 | [01:19.45]한 걸음 한 걸음 천천히 26 | [01:26.60] 27 | [01:32.49]하루만 더 지나면 괜찮아 질거야 28 | [01:36.52]자꾸만 주문처럼 외우는 혼잣말 29 | [01:40.59]거북아 널 볼 때면 내 모습 같아 30 | [01:44.65]눈물 나 미친 듯이 계속 흘러나와 31 | [01:48.69]새싹이 나겠지 32 | [01:50.44]꽃이 보이겠지 33 | [01:52.42]내 눈물의 사랑은 씨앗을 꼭 품겠지 34 | [01:56.89]내 가슴 깊이 하는 말 35 | [02:01.29]내게로 와요 36 | [02:03.75] 37 | [02:03.95]마음을 둘 곳도 없고 38 | [02:06.27]더 갈 곳도 없는 39 | [02:08.49]슬픈 거북이 한 마리 40 | [02:12.27]상처가 많아 너 혼자서 41 | [02:16.13]매일 외롭게 숨는 거니 42 | [02:20.17]너를 지킬 수 없고 43 | [02:22.53]더 사랑도 없는 44 | [02:24.60]내 가슴 아픈 이야기 45 | [02:28.94]조금 늦어도 좋아 46 | [02:32.66]한 걸음 한 걸음 천천히 47 | [02:37.67] 48 | [02:37.87]나보다 느린 네 발걸음에 49 | [02:41.67]맞춰 걸으며 50 | [02:44.71]더 이상 너 혼자 울지 않도록 51 | [02:51.17]you're always be my 52 | [02:53.46]always be my love 53 | [02:54.87] 54 | [02:55.08]마음을 둘 곳도 없고 55 | [02:57.20]더 갈 곳도 없는 56 | [02:59.27]슬픈 거북이 한 마리 57 | [03:01.87]제발 날 떠나지마 58 | [03:03.07]상처가 많아 너 혼자서 59 | [03:07.12]매일 외롭게 숨는 거니 60 | [03:11.02]너를 지킬 수 없고 61 | [03:13.39]더 사랑도 없는 62 | [03:15.42]내 가슴 아픈 이야기 63 | [03:19.73]조금 늦어도 좋아 64 | [03:23.57]한 걸음 한 걸음 천천히 65 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | //auther stefan 2 | var express = require('express'), 3 | path = require('path'), 4 | db = require('./routes/db'), 5 | album = require('./routes/album'), 6 | artist = require('./routes/artist'), 7 | user = require('./routes/user'), 8 | cms = require('./routes/cms'), 9 | music = require('./routes/music'), 10 | search = require('./routes/search'), 11 | lyric = require('./routes/lyric'); 12 | 13 | var app = express(); 14 | app.use(express.bodyParser()); 15 | app.use(express.static(path.join(__dirname, '../client/dist'))); 16 | 17 | app.all('*', function(req, res, next) { 18 | res.header("Access-Control-Allow-Origin", "*"); 19 | res.header("Access-Control-Allow-Headers", "accept, authentication, origin"); 20 | next(); 21 | }); 22 | 23 | app.get('/data/lyric/:file', lyric.getLyric); 24 | 25 | app.use('/data', express.static(path.join(__dirname, '/data'))); 26 | 27 | app.get('/album/:id', album.findById); 28 | app.get('/album', album.findAll); 29 | 30 | app.get('/music/:id', music.findById); 31 | app.get('/music', music.findAll); 32 | 33 | app.get('/artist/:id', artist.findById); 34 | app.get('/artist', artist.findAll); 35 | 36 | app.get('/search', search.search); 37 | 38 | // app.get('/user/:id', user.findById); 39 | app.get('/user/:id/fav', user.findFavs); 40 | app.get('/user/:id/fav/:favId', user.findFavById); 41 | // app.post('/user/:id/fav', user.addFav); 42 | // app.put('/user/:id/fav/:fid', user.updateFavById); 43 | 44 | app.get('/cms/slider', cms.findSliders); 45 | app.get('/cms/hot', cms.findHots); 46 | app.get('/cms/new', cms.findNews); 47 | 48 | //危险,请注意,访问此链接会导致数据库重置 49 | app.get('/initDB', db.resetDB); 50 | 51 | app.listen(1987); 52 | console.log('Listening port 1987...'); -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngmusic", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "express": "3.x", 7 | "grunt": "~0.4.2", 8 | "grunt-autoprefixer": "~0.4.0", 9 | "grunt-bower-install": "~0.7.0", 10 | "grunt-concurrent": "~0.4.1", 11 | "grunt-contrib-clean": "~0.5.0", 12 | "grunt-contrib-coffee": "~0.7.0", 13 | "grunt-contrib-compass": "~0.6.0", 14 | "grunt-contrib-concat": "~0.3.0", 15 | "grunt-contrib-connect": "~0.5.0", 16 | "grunt-contrib-copy": "~0.4.1", 17 | "grunt-contrib-cssmin": "~0.7.0", 18 | "grunt-contrib-htmlmin": "~0.1.3", 19 | "grunt-contrib-imagemin": "~0.5.0", 20 | "grunt-contrib-jshint": "~0.7.1", 21 | "grunt-contrib-uglify": "~0.2.0", 22 | "grunt-contrib-watch": "~0.5.2", 23 | "grunt-google-cdn": "~0.2.0", 24 | "grunt-newer": "~0.5.4", 25 | "grunt-ngmin": "~0.0.2", 26 | "grunt-rev": "~0.1.0", 27 | "grunt-svgmin": "~0.2.0", 28 | "grunt-usemin": "~2.0.0", 29 | "jshint-stylish": "~0.1.3", 30 | "load-grunt-tasks": "~0.2.0", 31 | "time-grunt": "~0.2.1", 32 | "karma-ng-html2js-preprocessor": "~0.1.0", 33 | "karma-ng-scenario": "~0.1.0", 34 | "karma-script-launcher": "~0.1.0", 35 | "karma-chrome-launcher": "~0.1.2", 36 | "karma-firefox-launcher": "~0.1.3", 37 | "karma-html2js-preprocessor": "~0.1.0", 38 | "karma-jasmine": "~0.1.5", 39 | "karma-coffee-preprocessor": "~0.1.2", 40 | "requirejs": "~2.1.10", 41 | "karma-requirejs": "~0.2.1", 42 | "karma-phantomjs-launcher": "~0.1.1", 43 | "karma": "~0.10.9", 44 | "grunt-karma": "~0.6.2", 45 | "grunt-require": "~0.1.0", 46 | "grunt-regex-replace": "~0.2.5" 47 | }, 48 | "engines": { 49 | "node": ">=0.8.0" 50 | }, 51 | "scripts": { 52 | "test": "grunt test" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/app/views/search.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/views/home.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 12 | 13 | 14 | 31 | 32 | 33 | 50 | 51 | 52 |
    -------------------------------------------------------------------------------- /client/app/styles/tiny.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | 3 | .tiny { 4 | position: fixed; 5 | z-index: 100; 6 | bottom: 0; 7 | width: 100%; 8 | height: 60px; 9 | @include box-shadow(#555 0px 0px -5px 0px); 10 | 11 | border-top: 2px solid #555; 12 | background: #fff; 13 | 14 | .cover { 15 | cursor: pointer; 16 | width: 50px; 17 | height: 50px; 18 | margin: 5px; 19 | } 20 | 21 | button { 22 | text-indent: -9999px; 23 | background-image: url('/images/player_buttons.png'); 24 | background-repeat: no-repeat; 25 | /* 120*120 */ 26 | width: 60px; 27 | height: 60px; 28 | @include scale(.8); 29 | background-size: 318px 185px; 30 | 31 | &.play { 32 | margin: 0 auto; 33 | background-position: -129px 0; 34 | 35 | &:hover { 36 | background-position: -129px -62px; 37 | } 38 | 39 | &.disable { 40 | background-position: -129px -125px; 41 | } 42 | 43 | } 44 | 45 | &.pause { 46 | margin: 0 auto; 47 | background-position: 0 0; 48 | &:hover { 49 | background-position: 0 -62px; 50 | } 51 | &.disable { 52 | background-position: 0 -125px; 53 | } 54 | } 55 | 56 | &.next { 57 | float: left; 58 | background-position: -192px 0; 59 | 60 | &:hover { 61 | background-position: -192px -62px; 62 | } 63 | 64 | &.disable { 65 | background-position: -192px -125px; 66 | } 67 | } 68 | 69 | &.prev { 70 | float: right; 71 | background-position: -257px 0; 72 | 73 | &:hover { 74 | background-position: -257px -62px; 75 | } 76 | 77 | &.disable { 78 | background-position: -257px -125px; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /server/routes/db.js: -------------------------------------------------------------------------------- 1 | var MongoClient = require('mongodb').MongoClient, 2 | Server = require('mongodb').Server, 3 | fs = require('fs'), 4 | path = require('path'), 5 | async = require('async'), 6 | db; 7 | 8 | var dataPath = path.join(__dirname, '../data/database') 9 | 10 | var mongoClient = new MongoClient(new Server('localhost', 27017)); 11 | 12 | mongoClient.open(function(err, mongoClient) { 13 | db = mongoClient.db("angular-music-db"); 14 | db.collection('songs', {strict:true}, function(err, collection) { 15 | if (err) { 16 | console.log('Initialize DataBase...'); 17 | createDB(); 18 | } 19 | }); 20 | }); 21 | 22 | exports.getDB = function() { 23 | return db; 24 | }; 25 | 26 | exports.resetDB = function(req, res) { 27 | db.dropDatabase(function() { 28 | createDB(function() { 29 | console.log('database reset done!'); 30 | res.write('done!'); 31 | res.end(); 32 | }); 33 | }); 34 | }; 35 | 36 | var createTableFromFile = function(tableName, filePath, cb) { 37 | fs.readFile(filePath, 'utf8', function(err, data) { 38 | if (err) throw err; 39 | data = JSON.parse(data); 40 | db.collection(tableName, function(err, collection) { 41 | collection.insert(data, {data:true}, function(err, result) { 42 | if (err) throw err; 43 | console.log('Initialized "' + tableName + '" Table...'); 44 | cb(); 45 | }); 46 | }); 47 | }); 48 | }; 49 | 50 | var createDB = function(callback) { 51 | 52 | async.parallel([ 53 | //初始化专辑 54 | function(cb) { 55 | createTableFromFile('albums', dataPath + '/albums.json', cb); 56 | }, 57 | //初始化歌曲 58 | function(cb) { 59 | createTableFromFile('songs', dataPath + '/songs.json', cb); 60 | }, 61 | //初始化歌手 62 | function(cb) { 63 | createTableFromFile('artists', dataPath + '/artists.json', cb); 64 | }, 65 | //初始化收藏 66 | function(cb) { 67 | createTableFromFile('users', dataPath + '/users.json', cb); 68 | } 69 | ], function(err, results) { 70 | if(err) throw err; 71 | callback && callback(); 72 | }); 73 | }; -------------------------------------------------------------------------------- /server/data/database/songs.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 0, 3 | "title": "答案", 4 | "artist": 0, 5 | "url": "data/music/答案.mp3", 6 | "publish": 20140130, 7 | "lyric": "data/lyric/答案.lrc", 8 | "album": 0 9 | },{ 10 | "id": 1, 11 | "title": "乌龟", 12 | "artist": 1, 13 | "url": "data/music/乌龟.mp3", 14 | "publish": 20130710, 15 | "lyric": "data/lyric/乌龟.lrc", 16 | "album": 1 17 | }, 18 | { 19 | "id": 2, 20 | "title": "信", 21 | "artist": 1, 22 | "url": "data/music/default.mp3", 23 | "publish": 20131212, 24 | "lyric": "data/lyric/default.lrc" 25 | }, 26 | { 27 | "id": 3, 28 | "title": "Wonder Woman", 29 | "artist": 1, 30 | "url": "data/music/default.mp3", 31 | "publish": 20131013, 32 | "lyric": "data/lyric/default.lrc", 33 | "album": 1 34 | }, 35 | { 36 | "id": 4, 37 | "title": "不知道吗", 38 | "artist": 1, 39 | "url": "data/music/default.mp3", 40 | "publish": 20130413, 41 | "lyric": "data/lyric/default.lrc", 42 | "album": 1 43 | }, 44 | { 45 | "id": 5, 46 | "title": "My Man", 47 | "artist": 1, 48 | "url": "data/music/default.mp3", 49 | "publish": 20130102, 50 | "lyric": "data/lyric/default.lrc", 51 | "album": 1 52 | }, 53 | { 54 | "id": 6, 55 | "title": "悲伤的约定", 56 | "artist": 1, 57 | "url": "data/music/default.mp3", 58 | "publish": 20130302, 59 | "lyric": "data/lyric/default.lrc", 60 | "album": 1 61 | }, 62 | { 63 | "id": 7, 64 | "title": "因为今天格外想你", 65 | "artist": 1, 66 | "url": "data/music/default.mp3", 67 | "publish": 20131015, 68 | "lyric": "data/lyric/default.lrc", 69 | "album": 1 70 | }, 71 | { 72 | "id": 8, 73 | "title": "悲伤的情歌", 74 | "artist": 1, 75 | "url": "data/music/default.mp3", 76 | "publish": 20130902, 77 | "lyric": "data/lyric/default.lrc", 78 | "album": 1 79 | }, 80 | { 81 | "id": 9, 82 | "title": "Troublemaker", 83 | "artist": 2, 84 | "url": "data/music/default.mp3", 85 | "publish": 20120402, 86 | "lyric": "data/lyric/default.lrc", 87 | "album": 2 88 | }] -------------------------------------------------------------------------------- /client/app/scripts/directives/flexDirective.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (directives) { 3 | 'use strict'; 4 | directives.directive('flexbox', function () { 5 | return { 6 | template: '
    ', 7 | restrict: 'EA', 8 | replace: true, 9 | transclude: true, 10 | link: function (scope, element, attrs) { 11 | 12 | var orient = attrs['boxOrient'], //horizontal, vertical 13 | direction = attrs['boxDirection'], //reverse, normal 14 | style = {}; 15 | 16 | if(orient) { 17 | style['-webkit-box-orient'] = orient; 18 | style['-moz-box-orient'] = orient; 19 | style['-ms-box-orient'] = orient; 20 | style['box-orient'] = orient; 21 | style['-webkit-flex-orient'] = orient; 22 | style['-moz-flex-orient'] = orient; 23 | style['-ms-flex-orient'] = orient; 24 | style['flex-orient'] = orient; 25 | } 26 | 27 | if(direction) { 28 | style['-webkit-box-direction'] = direction; 29 | style['-moz-box-direction'] = direction; 30 | style['-ms-box-direction'] = direction; 31 | style['flex-direction'] = direction; 32 | var newDirection = direction == 'reverse' ? 'row-reverse' : 'row'; 33 | style['-webkit-flex-direction'] = newDirection; 34 | style['-moz-flex-direction'] = newDirection; 35 | style['-ms-flex-direction'] = newDirection; 36 | style['flex-direction'] = newDirection; 37 | } 38 | 39 | element.css(style); 40 | } 41 | }; 42 | }).directive('flexitem', ['$interpolate', function ($interpolate) { 43 | return { 44 | template: '
    ', 45 | restrict: 'EA', 46 | transclude: true, 47 | replace: true, 48 | link: function (scope, element, attrs) { 49 | // var flex = $interpolate(attrs.flex)(scope); 50 | 51 | var flex = attrs.flex; 52 | 53 | if(flex) { 54 | if(/px$/.test(flex)) { 55 | element.css({ 56 | 'position': 'relative', 57 | 'width': flex 58 | }); 59 | } 60 | else { 61 | element.css({ 62 | 'position': 'relative', 63 | '-webkit-box-flex': flex, 64 | '-moz-box-flex': flex, 65 | '-ms-box-flex': flex, 66 | 'box-flex': flex, 67 | '-webkit-flex': flex, 68 | '-moz-flex': flex, 69 | '-ms-flex': flex, 70 | 'flex': flex, 71 | //防止元素内容对元素的宽度造成影响 72 | 'width': 0 73 | }); 74 | } 75 | } 76 | } 77 | }; 78 | }]); 79 | }); -------------------------------------------------------------------------------- /client/app/ui-router.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ui-router多维路由实例 5 | 6 | 7 | 8 | 9 |

    Base Navigation

    10 | 14 |
    15 | 23 | 27 | 31 | 39 | 43 | 47 | 91 | 92 | -------------------------------------------------------------------------------- /client/app/views/player.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 没有任何歌曲!我有点儿忧桑! 5 |
    6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
    53 | 77 | -------------------------------------------------------------------------------- /client/app/styles/search.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | .search { 3 | position: relative; 4 | height: 48px; 5 | 6 | .empty { 7 | width: 100%; 8 | height: 100%; 9 | font-size: 20px; 10 | margin-top: 50px; 11 | text-align: center; 12 | } 13 | 14 | .search-box { 15 | -webkit-box-shadow: 1px 1px 0 rgba(0,0,0,0.1),1px 1px 0 #fff inset; 16 | margin: 5px; 17 | height: 38px; 18 | @include background-image(linear-gradient(#666, #222)); 19 | z-index: 1; 20 | position: relative; 21 | 22 | .search-btn { 23 | position: relative; 24 | text-indent: -9999px; 25 | color: #fff; 26 | font-size: 16px; 27 | border-radius: 0 2px 2px 0; 28 | border: 1px solid #6e6e6e; 29 | height: 38px; 30 | margin-left: -1px; 31 | background: url('/images/search.png') center center no-repeat; 32 | background-size: 60% 60%; 33 | } 34 | 35 | dl.search-type{ 36 | color: #fff; 37 | font-size: 16px; 38 | border-radius: 2px 0 0 2px; 39 | border: 1px solid #6e6e6e; 40 | margin-right: -1px; 41 | background: -webkit-linear-gradient(top, #666 0%, #222 100%); 42 | 43 | dt, dd { 44 | height: 36px; 45 | text-align: center; 46 | line-height: 36px; 47 | font-size: 16px; 48 | @include background-image(linear-gradient(#666, #222)); 49 | } 50 | 51 | dt { 52 | background: url(/images/arrow_down.png) 92% center no-repeat; 53 | @include background-size(8px 4px); 54 | } 55 | } 56 | 57 | .search-input { 58 | background: #fff; 59 | height: 38px; 60 | border: 1px solid #6e6e6e; 61 | position: relative; 62 | 63 | input { 64 | font-size: 16px; 65 | outline: none; 66 | height: 21px; 67 | line-height: normal; 68 | position: absolute; 69 | top: 8px; 70 | border: none; 71 | right: 40px; 72 | left: 0; 73 | } 74 | } 75 | } 76 | 77 | ul.results { 78 | 79 | li { 80 | 81 | &:first-child { 82 | border-top: none; 83 | } 84 | 85 | height: 40px; 86 | border-top: 1px solid #fff; 87 | background: #ddd; 88 | border-bottom: 1px solid #9a9a9a; 89 | 90 | .name { 91 | padding-left: 20px; 92 | font-size: 18px; 93 | height: 40px; 94 | line-height: 40px; 95 | cursor: pointer; 96 | } 97 | 98 | .listen { 99 | background: url(../images/listen.png) center center no-repeat; 100 | @include background-size(20px 20px); 101 | } 102 | 103 | .album { 104 | background: url(../images/ret_album.png) center center no-repeat; 105 | @include background-size(20px 20px); 106 | } 107 | 108 | .artist { 109 | background: url(../images/ret_artist.png) center center no-repeat; 110 | @include background-size(26px 26px); 111 | } 112 | 113 | .add { 114 | background: url(../images/add.png) center center no-repeat; 115 | @include background-size(20px 20px); 116 | cursor: pointer; 117 | } 118 | } 119 | 120 | } 121 | } -------------------------------------------------------------------------------- /client/app/scripts/services/playerService.js: -------------------------------------------------------------------------------- 1 | define(['./module'], function (services) { 2 | 'use strict'; 3 | services.factory('playerService', ['$rootScope', function($rootScope) { 4 | 5 | var audio = new Audio, 6 | player, 7 | playList = [], 8 | playing = false, 9 | current = 0, 10 | updateCBArray = [], 11 | playCBArray = []; 12 | 13 | player = { 14 | 15 | play: function(song) { 16 | if(song) { 17 | playList = [song]; 18 | current = 0; 19 | audio.src = $rootScope.apiHost + '/' + song.url; 20 | } 21 | else if (playList[current].url != audio.src) { 22 | song = playList[current]; 23 | audio.src = $rootScope.apiHost + '/' + song.url; 24 | } 25 | 26 | if(song) { 27 | angular.forEach(playCBArray, function(cb) { 28 | cb.call(player, song, 0); 29 | }); 30 | $rootScope.cover = $rootScope.apiHost + '/data/artist/' + song.artist + '.jpg'; 31 | } 32 | 33 | audio.play(); 34 | playing = true; 35 | $rootScope.playEmpty = false; 36 | }, 37 | 38 | pause: function() { 39 | if (playing) { 40 | audio.pause(); 41 | playing = false; 42 | } 43 | }, 44 | 45 | next: function() { 46 | if (!playList.length || playList.length <= current + 1) return false; 47 | playing = true; 48 | current++; 49 | player.play(); 50 | return playList[current]; 51 | }, 52 | 53 | previous: function() { 54 | if (!playList.length || current <= 0) return false; 55 | playing = true; 56 | current--; 57 | player.play(); 58 | return playList[current]; 59 | }, 60 | 61 | add: function(songs) { 62 | if(!songs) return; 63 | var init = false; 64 | if(!angular.isArray(songs)) songs = [songs]; 65 | if(!playList.length) init = true; 66 | playList = playList.concat(songs); 67 | 68 | if(init) player.play(); 69 | }, 70 | 71 | onUpdate: function(cb) { 72 | angular.isFunction(cb) && updateCBArray.indexOf(cb) == -1 && updateCBArray.push(cb); 73 | }, 74 | 75 | onPlay: function(cb) { 76 | angular.isFunction(cb) && playCBArray.indexOf(cb) == -1 && playCBArray.push(cb); 77 | }, 78 | 79 | updateProgress: function(progress) { 80 | progress = Math.max(0, Math.min(progress, 100)); 81 | audio.currentTime = audio.duration * progress / 100; 82 | }, 83 | 84 | getPlayerInfo: function() { 85 | return { 86 | list: playList, 87 | playing: playing, 88 | progress: audio.currentTime ? audio.currentTime * 100 / audio.duration : 0, 89 | time: audio.currentTime || 0, 90 | current: current 91 | }; 92 | } 93 | }; 94 | 95 | angular.element(audio).on('timeupdate',function() { 96 | angular.forEach(updateCBArray, function(cb) { 97 | cb.call(player, (audio.currentTime * 100 / audio.duration).toFixed(2), audio.currentTime); 98 | }); 99 | }).on('ended', function() { 100 | player.next(); 101 | }); 102 | 103 | return player; 104 | }]); 105 | }); -------------------------------------------------------------------------------- /client/app/scripts/controllers/homeController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('homeController', ['$scope', '$rootScope', '$window', '$timeout', '$location', 'sliders', 'hots', 'news', 'playerService', 5 | function($scope, $rootScope, $window, $timeout, $location, sliders, hots, news, playerService) { 6 | 7 | $rootScope.title = '首页'; 8 | $rootScope.state = 'home'; 9 | 10 | $scope.sliders = sliders; 11 | $scope.hots = hots; 12 | $scope.news = news; 13 | 14 | var currentSlider = 0, 15 | sliderCount = sliders.length, 16 | listEl; 17 | 18 | $scope.touchSlider = function($event) { 19 | $event.gesture.preventDefault(); 20 | listEl = listEl || angular.element($event.target).parent().parent().parent(); 21 | }; 22 | 23 | $scope.swipeSlider = function($event) { 24 | $event.gesture.preventDefault(); 25 | if($event.gesture.direction == "left") { 26 | slideSliders(currentSlider + 1); 27 | } 28 | else { 29 | slideSliders(currentSlider - 1); 30 | } 31 | $event.gesture.stopDetect(); 32 | }; 33 | 34 | $scope.dragSlider = function($event) { 35 | $event.gesture.preventDefault(); 36 | var sliderOffset = -100 * (currentSlider / sliderCount), 37 | dragOffset = 100 * $event.gesture.deltaX / ($window.innerWidth * sliderCount); 38 | 39 | //开始和结束的幻灯片添加缓动 40 | if((currentSlider == 0 && $event.gesture.direction == "right") || 41 | (currentSlider == sliderCount - 1 && $event.gesture.direction == "left")) { 42 | dragOffset *= .5; 43 | } 44 | 45 | setOffset(dragOffset + sliderOffset); 46 | } 47 | 48 | $scope.releaseSlider = function($event) { 49 | $event.gesture.preventDefault(); 50 | if(Math.abs($event.gesture.deltaX) > $window.innerWidth / 3) { 51 | if($event.gesture.direction == 'left') { 52 | slideSliders(currentSlider + 1); 53 | } else { 54 | slideSliders(currentSlider - 1); 55 | } 56 | } 57 | else { 58 | slideSliders(currentSlider); 59 | } 60 | } 61 | 62 | $scope.add = function(music) { 63 | music.addClass = 'bling'; 64 | playerService.add(music); 65 | $timeout(function() { 66 | music.addClass = ''; 67 | }, 1000); 68 | }; 69 | 70 | initSliders(); 71 | 72 | angular.element($window).bind("load resize orientationchange", initSliders); 73 | 74 | function initSliders() { 75 | $scope.sliderWidth = $window.innerWidth; 76 | $scope.totalSliderWidth = $window.innerWidth * sliderCount; 77 | } 78 | 79 | function resizeSliders() { 80 | $scope.$apply(initSliders); 81 | } 82 | 83 | function slideSliders(dest) { 84 | currentSlider = Math.min(Math.max(dest, 0), sliderCount - 1); 85 | setOffset(-((100 / sliderCount) * currentSlider), true) 86 | } 87 | 88 | function setOffset(percent, animation) { 89 | listEl.removeClass('animate'); 90 | if(animation) { 91 | listEl.addClass('animate'); 92 | } 93 | listEl.css("-webkit-transform", "translate3d(" + percent + "%, 0, 0)"); 94 | listEl.css("transform", "translate3d(" + percent + "%, 0, 0)"); 95 | //可添加更多浏览器的支持 96 | } 97 | 98 | }]); 99 | }); -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | //author stefan 2 | //这里为了简便我们将所有的用户都指向了名为stefan的用户信息 3 | var DB = require('./db'), 4 | db; 5 | 6 | exports.findFavs = function(req, res) { 7 | db = DB.getDB(); 8 | var id = +req.params.id, artistIdArr = []; 9 | db.collection('users', function(err, collection) { 10 | collection.findOne({'id': 1}, function(err, item) { 11 | if(!item.songs) { 12 | res.json([]); 13 | } 14 | else { 15 | db.collection('songs', function(err, collection) { 16 | collection.find({id: {$in: item.songs}}).toArray(function(err, items) { 17 | items.forEach(function(item) { 18 | artistIdArr.push(item.artist); 19 | }); 20 | db.collection('artists', function(err, collection) { 21 | collection.find({id: {$in: artistIdArr}}).toArray(function(err, ret) { 22 | items.forEach(function(item, index) { 23 | ret.forEach(function(retItem) { 24 | if(retItem['id'] == item.artist) { 25 | item.artistName = retItem['name']; 26 | } 27 | }) 28 | }); 29 | 30 | res.json(items); 31 | }); 32 | }); 33 | }); 34 | }); 35 | } 36 | }); 37 | }); 38 | }; 39 | 40 | exports.findFavById = function(req, res) { 41 | db = DB.getDB(); 42 | var id = +req.params.id, 43 | favId = +req.params.favId; 44 | db.collection('users', function(err, collection) { 45 | collection.findOne({'id': 1}, function(err, item) { 46 | var songs = item.songs; 47 | if(songs.indexOf(favId) == -1) { 48 | res.end(null); 49 | } 50 | else { 51 | db.collection('songs', function(err, collection) { 52 | collection.findOne({'id': favId}, function(err, item) { 53 | res.json(item); 54 | }); 55 | }); 56 | } 57 | }); 58 | }); 59 | }; 60 | 61 | exports.addFav = function(req, res) { 62 | db = DB.getDB(); 63 | var id = +req.params.id, 64 | favId = +req.params.favId; 65 | db.collection('users', function(err, collection) { 66 | collection.findOne({'id': 1}, function(err, item) { 67 | var songs = item.songs; 68 | if(songs.indexOf(favId) == -1) { 69 | songs.push(favId); 70 | } 71 | collection.update({'id': 1}, { 72 | $set: {songs: songs} 73 | }, function(err, item) { 74 | res.end(); 75 | }); 76 | }); 77 | }); 78 | }; 79 | 80 | exports.deleteFav = function(req, res) { 81 | db = DB.getDB(); 82 | var id = +req.params.id, 83 | favId = +req.params.favId; 84 | db.collection('users', function(err, collection) { 85 | collection.findOne({'id': 1}, function(err, item) { 86 | var songs = item.songs, 87 | index = songs.indexOf(favId); 88 | if(index > 0) { 89 | songs.splice(index, 1); 90 | } 91 | collection.update({'id': 1}, { 92 | $set: {songs: songs} 93 | }, function(err, item) { 94 | res.end(); 95 | }); 96 | }); 97 | }); 98 | }; 99 | 100 | 101 | -------------------------------------------------------------------------------- /client/app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | html, body { 3 | height: 100%; 4 | -webkit-user-select: none; 5 | } 6 | 7 | body, p, div, button, header, dl, dt, dd, input, ul { 8 | margin: 0; 9 | padding: 0; 10 | @include box-sizing(border-box); 11 | } 12 | 13 | button { 14 | position: relative; 15 | border: 0; 16 | margin: 0; 17 | background: transparent; 18 | cursor: pointer; 19 | display: block; 20 | cursor: pointer; 21 | } 22 | 23 | //for flexbox directive 24 | 25 | .flexbox { 26 | width: 100%; 27 | @include display-box; 28 | @include box-align(stretch); 29 | } 30 | 31 | .flexitem { 32 | position: relative; 33 | width: 0; 34 | } 35 | 36 | //for tab directive 37 | 38 | .tabs { 39 | 40 | width: 100%; 41 | 42 | ul.tabs-nav { 43 | 44 | z-index: 1; 45 | position: relative; 46 | 47 | @include display-box; 48 | 49 | li { 50 | 51 | list-style: none; 52 | @include box-flex(4); 53 | line-height: 25px; 54 | text-align: center; 55 | margin: 2px 2px -1px; 56 | font-size: 18px; 57 | 58 | a { 59 | display: block; 60 | width: 100%; 61 | text-decoration: none; 62 | cursor: pointer; 63 | line-height: 25px; 64 | color: #000; 65 | } 66 | 67 | &.active { 68 | @include border-radius(2px 2px 0 0); 69 | border-width: 1px 1px 0 1px; 70 | border-color: #9a9a9a; 71 | border-style: solid; 72 | background: #ddd; 73 | line-height: 18px; 74 | } 75 | 76 | } 77 | } 78 | 79 | .tabs-content { 80 | border-top: 1px solid #9a9a9a; 81 | } 82 | } 83 | 84 | .container { 85 | padding: 42px 0 62px; 86 | } 87 | 88 | //for bling animation 89 | 90 | @include keyframes(bling-keyframes){ 91 | 0%{ 92 | opacity: 0; 93 | } 94 | 100% { 95 | opacity: 1; 96 | } 97 | } 98 | 99 | .bling-add, .bling-remove { 100 | @include transition-property(all); 101 | @include transition-duration(.5s); 102 | @include transition-timing-function(ease-in); 103 | } 104 | 105 | .bling, .bling-add.bing-add-active { 106 | color: red; 107 | @include animation(bling-keyframes .5s linear infinite); 108 | } 109 | 110 | .bling-remove.bling-remove-active { 111 | color: black; 112 | } 113 | 114 | //for slide animation 115 | 116 | .slide-left.ng-enter, 117 | .slide-left.ng-leave, 118 | .slide-right.ng-enter, 119 | .slide-right.ng-leave { 120 | position: absolute; 121 | top: 0; 122 | right: 0; 123 | bottom: 0; 124 | left: 0; 125 | @include transition-property(all); 126 | @include transition-duration(.2s); 127 | @include transition-timing-function(ease-in-out); 128 | } 129 | 130 | 131 | .slide-left { 132 | 133 | &.ng-enter { 134 | @include translate3d(100%, 0, 0); 135 | 136 | &.ng-enter-active { 137 | @include translate3d(0, 0, 0); 138 | } 139 | } 140 | 141 | &.ng-leave { 142 | @include translate3d(0, 0, 0); 143 | 144 | &.ng-leave-active { 145 | @include translate3d(-100%, 0, 0); 146 | } 147 | } 148 | } 149 | 150 | .slide-right { 151 | 152 | &.ng-enter { 153 | @include translate3d(-100%, 0, 0); 154 | 155 | &.ng-enter-active { 156 | @include translate3d(0, 0, 0); 157 | } 158 | } 159 | 160 | &.ng-leave { 161 | @include translate3d(0, 0, 0); 162 | 163 | &.ng-leave-active { 164 | @include translate3d(100%, 0, 0); 165 | } 166 | } 167 | } 168 | 169 | 170 | @import 'toolbar.scss'; 171 | @import 'search.scss'; 172 | @import 'player.scss'; 173 | @import 'home.scss'; 174 | @import 'user.scss'; 175 | @import 'album.scss'; 176 | @import 'tiny.scss'; 177 | @import 'artist.scss'; -------------------------------------------------------------------------------- /client/app/scripts/services/restService.js: -------------------------------------------------------------------------------- 1 | define(['./module'], function (services) { 2 | 'use strict'; 3 | 4 | //Music API 5 | services.factory('musicAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 6 | 7 | var host = $rootScope.apiHost; 8 | 9 | var resource = $resource(host + '/music/:id', { 10 | id: '@id' 11 | }); 12 | 13 | return { 14 | fetchAll: function() { 15 | return resource.query(); 16 | }, 17 | 18 | fetchById: function(id) { 19 | return resource.get({id: id}); 20 | } 21 | }; 22 | }]) 23 | //artist API 24 | .factory('artistAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 25 | var host = $rootScope.apiHost; 26 | 27 | var resource = $resource(host + '/artist/:id', { 28 | id: '@id' 29 | }); 30 | 31 | return { 32 | fetchAll: function() { 33 | return resource.query(); 34 | }, 35 | 36 | fetchById: function(id) { 37 | return resource.get({id: id}); 38 | } 39 | }; 40 | }]) 41 | //user API 42 | .factory('userAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 43 | 44 | var host = $rootScope.apiHost; 45 | 46 | var resource = $resource(host + '/user/:userId/fav/:favId', { 47 | favId: '@id' 48 | }); 49 | 50 | 51 | return { 52 | fetchAllFavs: function(userId) { 53 | return resource.query({userId: userId}); 54 | }, 55 | 56 | saveFav: function(musicId) { 57 | 写死用户id 58 | var FavResource = $resource(host + '/user/:userId/fav/:favId', { 59 | userId: 1 60 | }); 61 | var r = new FavResource({musicId: 1}); 62 | r.musicName = '泡沫'; 63 | r.$save(); 64 | }, 65 | 66 | getFav: function(musicId) { 67 | var FavResource = $resource(host + '/user/:userId/fav/:favId', { 68 | userId: 1, 69 | favId: '@id' 70 | }); 71 | FavResource.get({favId: musicId}, function(favMusic) { 72 | expect(favMusic instanceof FavResource).toEqual(true); 73 | }); 74 | } 75 | }; 76 | }]) 77 | //album API 78 | .factory('albumAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 79 | 80 | var host = $rootScope.apiHost; 81 | 82 | var resource = $resource(host + '/album/:id', { 83 | id: '@id' 84 | }); 85 | 86 | return { 87 | fetchAll: function() { 88 | return resource.query(); 89 | }, 90 | 91 | fetchById: function(id) { 92 | return resource.get({id: id}); 93 | } 94 | }; 95 | }]) 96 | //search API 97 | .factory('searchAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 98 | 99 | var host = $rootScope.apiHost; 100 | 101 | var buildQuery = function(o) { 102 | var ret = [], i; 103 | for(i in o) { 104 | ret.push(i + '=' + o[i]); 105 | } 106 | return ret.join('&'); 107 | }, 108 | mapType = function(type) { 109 | return ['music', 'artist', 'album'].indexOf(type); 110 | }; 111 | 112 | return { 113 | search: function (o) { 114 | return $resource(host + '/search' + '?' + buildQuery(o), {}).query(); 115 | } 116 | }; 117 | }]) 118 | //CMS API 119 | .factory('cmsAPI', ['$resource', '$q', '$rootScope', function($resource, $q, $rootScope) { 120 | 121 | var host = $rootScope.apiHost; 122 | 123 | return { 124 | 125 | fetchSliders: function() { 126 | return $resource(host + '/cms/slider').query(); 127 | }, 128 | 129 | fetchHots: function(id) { 130 | return $resource(host + '/cms/hot', {}).query(); 131 | }, 132 | 133 | fetchNews: function(id) { 134 | //如下是自定义方法的例子 135 | // var Res = $resource('/resource/:id', null, 136 | // { 137 | // 'update': { 138 | // method:'PUT', 139 | // params: { 140 | // tag: 2 141 | // } 142 | // } 143 | // }); 144 | // var res = Res.update({id: 1}, {key1: 1, key2: 2}); 145 | return $resource(host + '/cms/new', {}).query(); 146 | } 147 | }; 148 | }]); 149 | }); -------------------------------------------------------------------------------- /client/app/scripts/controllers/playerController.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(['./module'], function (controllers) { 3 | 'use strict'; 4 | controllers.controller('playerController', ['$scope', '$rootScope', '$window', '$interval', '$http', '$location', '$routeParams', 'playerService', 'userAPI', 'music', 5 | function ($scope, $rootScope, $window, $interval, $http, $location, $routeParams, playerService, userAPI, music) { 6 | 7 | var progressPassedEl, 8 | progressWidth = 0, 9 | intervalId, 10 | playList, 11 | info = {}; 12 | 13 | $rootScope.title = '播放器'; 14 | $rootScope.state = 'player'; 15 | 16 | $scope.empty = true; 17 | 18 | $scope.lyric = {}; 19 | $scope.music = {}; 20 | 21 | $scope.track = { 22 | cover: '', 23 | degrees: 0, 24 | sub: $routeParams.sub == 'lyric' ? 'lyric' : 'cover' 25 | }; 26 | 27 | //播放列表 28 | $scope.playList = []; 29 | 30 | $scope.player = { 31 | 32 | playing: true, 33 | 34 | play: function() { 35 | 36 | if(this.playing) { 37 | playerService.pause(); 38 | } 39 | else { 40 | playerService.play(); 41 | } 42 | 43 | this.playing = !this.playing; 44 | 45 | }, 46 | next: function() { 47 | var nextSong = playerService.next(); 48 | }, 49 | previous: function() { 50 | var previousSong = playerService.previous(); 51 | } 52 | }; 53 | 54 | $scope.progress = { 55 | 56 | musicProgress: 0, 57 | 58 | touchProgress: function($event) { 59 | playerService.pause(); 60 | $event.gesture.preventDefault(); 61 | progressPassedEl = angular.element(document.getElementById('progress_passed')); 62 | progressWidth = progressPassedEl[0].clientWidth; 63 | }, 64 | 65 | dragProgress: function($event) { 66 | $event.gesture.preventDefault(); 67 | var currentWidth = progressWidth + $event.gesture.deltaX; 68 | progressPassedEl.css('width', progressWidth + $event.gesture.deltaX + 'px'); 69 | 70 | return currentWidth; 71 | 72 | // return passedEl; 73 | }, 74 | 75 | releaseProgress: function($event) { 76 | playerService.updateProgress(this.dragProgress($event) * 100 / $window.innerWidth); 77 | playerService.play(); 78 | } 79 | }; 80 | 81 | $scope.goAlbum = function() { 82 | if($scope.music && angular.isDefined($scope.music.album)) 83 | $location.path('/album/' + $scope.music.album); 84 | }; 85 | 86 | $scope.addFav = function() { 87 | //TODO 88 | userAPI.saveFav(1); 89 | }; 90 | 91 | playerService.onUpdate(function(progress, time) { 92 | $scope.$apply(function() { 93 | $scope.progress.musicProgress = progress + '%'; 94 | $scope.lyric.time = time; 95 | }); 96 | }); 97 | 98 | playerService.onPlay(function(progress, time) { 99 | $scope.track.cover = $rootScope.apiHost + '/data/artist/' + music.artist + '.jpg'; 100 | }); 101 | 102 | if(music) { 103 | $scope.music = music; 104 | playerService.play(music); 105 | 106 | $http.get($rootScope.apiHost + '/' + music.lyric).success(function(data) { 107 | console.info(arguments); 108 | $scope.lyric.src = data; 109 | }); 110 | } 111 | 112 | info = playerService.getPlayerInfo(); 113 | 114 | if(info.list.length) { 115 | $scope.empty = false; 116 | 117 | if(!music) { 118 | $scope.music = music = info.list[info.current]; 119 | 120 | $http.get($rootScope.apiHost + '/' + music.lyric).success(function(data) { 121 | $scope.lyric.src = data; 122 | }); 123 | } 124 | 125 | } 126 | 127 | $scope.progress.musicProgress = info.progress + '%'; 128 | $scope.lyric.time = info.time; 129 | $scope.track.cover = $rootScope.cover; 130 | 131 | 132 | //使用$interval可以省去$apply操作 133 | intervalId = $interval(function() { 134 | if($scope.player.playing) updateLP(); 135 | }, 50); 136 | 137 | function updateLP() { 138 | $scope.track.degrees = ($scope.track.degrees + 10) % 360; 139 | } 140 | 141 | $scope.$on('$destroy', function () { 142 | $interval.cancel(intervalId); 143 | }); 144 | 145 | }]); 146 | }); -------------------------------------------------------------------------------- /client/app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
    144 |

    Not found :(

    145 |

    Sorry, but the page you were trying to view does not exist.

    146 |

    It looks like this was the result of either:

    147 | 151 | 154 | 155 |
    156 | 157 | 158 | -------------------------------------------------------------------------------- /client/app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'angular', 4 | 'controllers/controllers', 5 | 'directives/directives', 6 | 'filters/filters', 7 | 'services/services', 8 | 'animations/animations' 9 | ], function (angular) { 10 | 'use strict'; 11 | return angular.module('ngMusic', [ 12 | 'ngRoute', 13 | 'ngTouch', 14 | 'ngAnimate', 15 | 'angular-gestures', 16 | 'ngResource', 17 | 'ngMusic.controllers', 18 | 'ngMusic.directives', 19 | 'ngMusic.services', 20 | 'ngMusic.filters' 21 | // 'ngMusic.animations' 22 | ]).config(['$routeProvider', '$httpProvider', 23 | function($routeProvider, $httpProvider) { 24 | $routeProvider.when('/home', { 25 | templateUrl: '../views/home.html', 26 | controller: 'homeController', 27 | resolve: { 28 | sliders: ['cmsAPI', function(cmsAPI) { 29 | return cmsAPI.fetchSliders().$promise; 30 | }], 31 | hots: ['cmsAPI', function(cmsAPI) { 32 | return cmsAPI.fetchHots().$promise; 33 | }], 34 | news: ['cmsAPI', function(cmsAPI) { 35 | return cmsAPI.fetchNews().$promise; 36 | }] 37 | } 38 | }).when('/player', { 39 | templateUrl: '../views/player.html', 40 | controller: 'playerController', 41 | resolve: { 42 | music: function() { 43 | return false; 44 | } 45 | } 46 | }).when('/player/:id', { 47 | templateUrl: '../views/player.html', 48 | controller: 'playerController', 49 | resolve: { 50 | music: ['musicAPI', '$route', function(musicAPI, $route) { 51 | return musicAPI.fetchById($route.current.params.id).$promise; 52 | }] 53 | } 54 | }).when('/search', { 55 | templateUrl: '../views/search.html', 56 | controller: 'searchController' 57 | }).when('/user/:id', { 58 | templateUrl: '../views/user.html', 59 | controller: 'userController', 60 | resolve: { 61 | favs: ['userAPI', '$route', function(userAPI, $route) { 62 | return userAPI.fetchAllFavs($route.current.params.id); 63 | }] 64 | } 65 | }).when('/album/:id', { 66 | templateUrl: '../views/album.html', 67 | controller: 'albumController' 68 | }).when('/artist/:id', { 69 | templateUrl: '../views/artist.html', 70 | controller: 'artistController', 71 | resolve: { 72 | artist: ['artistAPI', '$route', function(artistAPI, $route) { 73 | return artistAPI.fetchById($route.current.params.id).$promise; 74 | }], 75 | musics: ['searchAPI', '$route', function(searchAPI, $route) { 76 | return searchAPI.search({ 77 | type: 'music', 78 | artist: $route.current.params.id 79 | }).$promise; 80 | }], 81 | albums: ['searchAPI', '$route', function(searchAPI, $route) { 82 | return searchAPI.search({ 83 | type: 'album', 84 | artist: $route.current.params.id 85 | }).$promise; 86 | }] 87 | } 88 | }).otherwise({ 89 | redirectTo: '/home' 90 | }); 91 | 92 | //修改默认配置的示例 93 | $httpProvider.defaults.cache = true; 94 | $httpProvider.defaults.headers.common.Authentication = 'Basic YmVlcDpib29w'; 95 | } 96 | ]).run(['$rootScope', '$location', '$q', function($rootScope, $location, $q) { 97 | 98 | $rootScope.apiHost = 'http://localhost:1987'; 99 | $rootScope.playEmpty = true; 100 | $rootScope.slide = 'slide-left'; 101 | 102 | $rootScope.back = function() { 103 | $rootScope.slide = 'slide-right'; 104 | $window.history.back(); 105 | } 106 | $rootScope.go = function(path){ 107 | $rootScope.slide = 'slide-left'; 108 | $location.url(path); 109 | } 110 | 111 | //以下代码用于说明$q异步服务的作用 112 | // function a() { 113 | // var d = $q.defer(); 114 | // setTimeout(function() { 115 | // console.info('a'); 116 | // d.resolve('a'); 117 | // }, 100); 118 | // return d.promise; 119 | // } 120 | // function b() { 121 | // console.info(arguments); 122 | // var d = $q.defer(); 123 | // setTimeout(function() { 124 | // console.info('b'); 125 | // d.resolve('b'); 126 | // }, 100); 127 | // return d.promise; 128 | // } 129 | // function c() { 130 | // console.info(arguments); 131 | // var d = $q.defer(); 132 | // setTimeout(function() { 133 | // console.info('c'); 134 | // d.resolve('c'); 135 | // }, 100); 136 | // return d.promise; 137 | // } 138 | 139 | // var d = $q.defer(); 140 | 141 | // a().then(b).then(c).then(function() { 142 | // console.info(arguments); 143 | // console.info('success'); 144 | // }); 145 | 146 | }]); 147 | }); 148 | -------------------------------------------------------------------------------- /client/app/styles/player.scss: -------------------------------------------------------------------------------- 1 | // need compass > 0.13 2 | @import "compass/css3"; 3 | 4 | @include keyframes(rotateFrame){ 5 | 0%{ 6 | @include rotate(0deg); 7 | } 8 | 100% { 9 | @include rotate(360deg); 10 | } 11 | } 12 | 13 | .rotate-1 { 14 | @include animation(rotateFrame 5s linear infinite); 15 | } 16 | 17 | .rotate-2 { 18 | @include animation(rotateFrame 3s linear infinite); 19 | } 20 | 21 | .rotate-3 { 22 | @include animation(rotateFrame 1s linear infinite); 23 | } 24 | 25 | .player { 26 | 27 | .empty { 28 | width: 100%; 29 | height: 100%; 30 | font-size: 20px; 31 | margin-top: 50px; 32 | text-align: center; 33 | } 34 | 35 | .switcher { 36 | 37 | height: 200px; 38 | width: 100%; 39 | position: relative; 40 | 41 | margin: 10px 0; 42 | 43 | .track { 44 | 45 | .lp { 46 | display: block; 47 | position: relative; 48 | margin: 0 auto; 49 | width: 200px; 50 | height: 200px; 51 | background: url(/images/LP.png) center center no-repeat; 52 | background-size: 100% 100%; 53 | 54 | .cover { 55 | border-radius: 30px; 56 | position: absolute; 57 | top: 50%; 58 | left: 50%; 59 | margin-left: -30px; 60 | margin-top: -30px; 61 | width: 60px; 62 | height: 60px; 63 | background-position: center center; 64 | background-repeat: no-repeat; 65 | @include background-size(100% 100%); 66 | } 67 | } 68 | } 69 | 70 | .lyric { 71 | 72 | height: 100%; 73 | 74 | .lyric-list { 75 | 76 | overflow: hidden; 77 | width: 100%; 78 | height: 100%; 79 | position: relative; 80 | 81 | ul { 82 | width: 100%; 83 | position: absolute; 84 | 85 | li { 86 | width: 100%; 87 | padding: 0 20px; 88 | line-height: 16px; 89 | font-size: 10px; 90 | height: 16px; 91 | width: 100%; 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | white-space: nowrap; 95 | text-align: center; 96 | } 97 | } 98 | 99 | } 100 | } 101 | 102 | } 103 | 104 | .progress { 105 | 106 | width: 100%; 107 | margin: 10px 0; 108 | 109 | .bar { 110 | width: 100%; 111 | height: 4px; 112 | -webkit-box-sizing: border-box; 113 | -moz-box-sizing: border-box; 114 | box-sizing: border-box; 115 | /* 15px buffer */ 116 | padding: 0 8px 0 0; 117 | background: #d9d9d9; 118 | position: relative; 119 | 120 | .pass { 121 | background: #555; 122 | height: 4px; 123 | padding-left: 8px; 124 | width: 0; 125 | 126 | .thumb { 127 | width: 30px; 128 | height: 30px; 129 | position: absolute; 130 | top: -13px; 131 | right: -15px; 132 | 133 | span { 134 | display: block; 135 | width: 16px; 136 | height: 16px; 137 | color: #ddd; 138 | border: 3px solid #999; 139 | border-radius: 8px; 140 | background: #ddd; 141 | top: 50%; 142 | left: 50%; 143 | margin: -8px; 144 | position: absolute; 145 | box-sizing: border-box; 146 | } 147 | 148 | } 149 | } 150 | } 151 | } 152 | 153 | .extra { 154 | margin: 20px 0; 155 | 156 | button { 157 | padding: 0; 158 | height: 30px; 159 | border-style: solid; 160 | font-weight: bold; 161 | line-height: 1; 162 | margin: 0 auto; 163 | text-align: center; 164 | font-size: 12px; 165 | background: #222; 166 | background: -webkit-linear-gradient(top, #666 0%, #222 100%); 167 | border-color: #000; 168 | color: white; 169 | -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5); 170 | box-sizing: border-box; 171 | 172 | &.singer, &.fav { 173 | width: 100%; 174 | border-width: 2px 1px 2px 1px; 175 | } 176 | 177 | &.switch { 178 | float: right; 179 | width: 50%; 180 | border-radius: 4px 0 0 4px; 181 | border-width: 2px 1px 2px 2px; 182 | } 183 | 184 | &.album { 185 | float: left; 186 | width: 50%; 187 | border-radius: 0 4px 4px 0; 188 | border-width: 2px 2px 2px 1px; 189 | } 190 | 191 | &:hover { 192 | background: #555; 193 | background: -webkit-linear-gradient(top, #999 0%, #555 100%); 194 | border-color: #333; 195 | } 196 | 197 | &.disable { 198 | background: #999; 199 | border-color: #777; 200 | cursor: default; 201 | } 202 | 203 | } 204 | } 205 | 206 | .control { 207 | 208 | margin: 20px 0; 209 | 210 | button { 211 | text-indent: -9999px; 212 | background-image: url('/images/player_buttons.png'); 213 | background-repeat: no-repeat; 214 | /* 120*120 */ 215 | width: 60px; 216 | height: 60px; 217 | background-size: 318px 185px; 218 | 219 | &.play { 220 | margin: 0 auto; 221 | background-position: -129px 0; 222 | 223 | &:hover { 224 | background-position: -129px -62px; 225 | } 226 | 227 | } 228 | 229 | &.pause { 230 | margin: 0 auto; 231 | background-position: 0 0; 232 | &:hover { 233 | background-position: 0 -62px; 234 | } 235 | } 236 | 237 | &.next { 238 | float: left; 239 | background-position: -192px 0; 240 | 241 | &:hover { 242 | background-position: -192px -62px; 243 | } 244 | 245 | } 246 | 247 | &.prev { 248 | float: right; 249 | background-position: -257px 0; 250 | 251 | &:hover { 252 | background-position: -257px -62px; 253 | } 254 | 255 | } 256 | 257 | } 258 | 259 | } 260 | } -------------------------------------------------------------------------------- /client/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-01-21 using generator-angular 0.7.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Define the configuration for all the tasks 19 | grunt.initConfig({ 20 | 21 | // Project settings 22 | yeoman: { 23 | // configurable paths 24 | app: require('./bower.json').appPath || 'app', 25 | dist: 'dist' 26 | }, 27 | 28 | // Watches files for changes and runs tasks based on the changed files 29 | watch: { 30 | js: { 31 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 32 | tasks: ['newer:jshint:all'], 33 | options: { 34 | livereload: true 35 | } 36 | }, 37 | jsTest: { 38 | files: ['test/spec/{,*/}*.js'], 39 | tasks: ['newer:jshint:test', 'karma'] 40 | }, 41 | compass: { 42 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 43 | tasks: ['compass:server', 'autoprefixer'] 44 | }, 45 | gruntfile: { 46 | files: ['Gruntfile.js'] 47 | }, 48 | livereload: { 49 | options: { 50 | livereload: '<%= connect.options.livereload %>' 51 | }, 52 | files: [ 53 | '<%= yeoman.app %>/{,*/}*.html', 54 | '.tmp/styles/{,*/}*.css', 55 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 56 | ] 57 | } 58 | }, 59 | 60 | // The actual grunt server settings 61 | connect: { 62 | options: { 63 | port: 9000, 64 | // Change this to '0.0.0.0' to access the server from outside. 65 | hostname: '0.0.0.0', 66 | // hostname: 'localhost', 67 | livereload: 35729 68 | }, 69 | livereload: { 70 | options: { 71 | open: true, 72 | base: [ 73 | '.tmp', 74 | '<%= yeoman.app %>' 75 | ] 76 | } 77 | }, 78 | test: { 79 | options: { 80 | port: 9001, 81 | base: [ 82 | '.tmp', 83 | 'test', 84 | '<%= yeoman.app %>' 85 | ] 86 | } 87 | }, 88 | dist: { 89 | options: { 90 | base: '<%= yeoman.dist %>' 91 | } 92 | } 93 | }, 94 | 95 | // Make sure code styles are up to par and there are no obvious mistakes 96 | jshint: { 97 | options: { 98 | jshintrc: '.jshintrc', 99 | reporter: require('jshint-stylish') 100 | }, 101 | all: [ 102 | 'Gruntfile.js', 103 | '<%= yeoman.app %>/scripts/{,*/}*.js' 104 | ], 105 | test: { 106 | options: { 107 | jshintrc: 'test/.jshintrc' 108 | }, 109 | src: ['test/spec/{,*/}*.js'] 110 | } 111 | }, 112 | 113 | // Empties folders to start fresh 114 | clean: { 115 | dist: { 116 | files: [{ 117 | dot: true, 118 | src: [ 119 | '.tmp', 120 | '<%= yeoman.dist %>/*', 121 | '!<%= yeoman.dist %>/.git*' 122 | ] 123 | }] 124 | }, 125 | server: '.tmp' 126 | }, 127 | 128 | // Add vendor prefixed styles 129 | autoprefixer: { 130 | options: { 131 | browsers: ['last 1 version'] 132 | }, 133 | dist: { 134 | files: [{ 135 | expand: true, 136 | cwd: '.tmp/styles/', 137 | src: '{,*/}*.css', 138 | dest: '.tmp/styles/' 139 | }] 140 | } 141 | }, 142 | 143 | // Automatically inject Bower components into the app 144 | 'bower-install': { 145 | app: { 146 | html: '<%= yeoman.app %>/index.html', 147 | ignorePath: '<%= yeoman.app %>/' 148 | } 149 | }, 150 | 151 | // Compiles Sass to CSS and generates necessary files if requested 152 | compass: { 153 | // options: { 154 | // config: 'config.rb' 155 | // sassDir: '<%= yeoman.app %>/styles', 156 | // cssDir: '.tmp/styles', 157 | // generatedImagesDir: '.tmp/images/generated', 158 | // imagesDir: '<%= yeoman.app %>/images', 159 | // javascriptsDir: '<%= yeoman.app %>/scripts', 160 | // fontsDir: '<%= yeoman.app %>/styles/fonts', 161 | // importPath: '<%= yeoman.app %>/styles', 162 | // httpImagesPath: '/images', 163 | // httpGeneratedImagesPath: '/images/generated', 164 | // httpFontsPath: '/styles/fonts', 165 | // relativeAssets: false, 166 | // assetCacheBuster: false, 167 | // raw: 'Sass::Script::Number.precision = 10\n' 168 | // }, 169 | dist: { 170 | options: { 171 | config: 'config.rb' 172 | // generatedImagesDir: '<%= yeoman.dist %>/images/generated' 173 | } 174 | }, 175 | server: { 176 | options: { 177 | config: 'config.rb', 178 | debugInfo: true 179 | } 180 | } 181 | }, 182 | 183 | // Renames files for browser caching purposes 184 | rev: { 185 | dist: { 186 | files: { 187 | src: [ 188 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 189 | '<%= yeoman.dist %>/styles/{,*/}*.css', 190 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 191 | '<%= yeoman.dist %>/styles/fonts/*' 192 | ] 193 | } 194 | } 195 | }, 196 | 197 | // Reads HTML for usemin blocks to enable smart builds that automatically 198 | // concat, minify and revision files. Creates configurations in memory so 199 | // additional tasks can operate on them 200 | useminPrepare: { 201 | html: '<%= yeoman.app %>/index.html', 202 | options: { 203 | dest: '<%= yeoman.dist %>' 204 | } 205 | }, 206 | 207 | // Performs rewrites based on rev and the useminPrepare configuration 208 | usemin: { 209 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 210 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 211 | options: { 212 | assetsDirs: ['<%= yeoman.dist %>'] 213 | } 214 | }, 215 | 216 | // The following *-min tasks produce minified files in the dist folder 217 | imagemin: { 218 | dist: { 219 | files: [{ 220 | expand: true, 221 | cwd: '<%= yeoman.app %>/images', 222 | src: '{,*/}*.{png,jpg,jpeg,gif}', 223 | dest: '<%= yeoman.dist %>/images' 224 | }] 225 | } 226 | }, 227 | svgmin: { 228 | dist: { 229 | files: [{ 230 | expand: true, 231 | cwd: '<%= yeoman.app %>/images', 232 | src: '{,*/}*.svg', 233 | dest: '<%= yeoman.dist %>/images' 234 | }] 235 | } 236 | }, 237 | htmlmin: { 238 | dist: { 239 | options: { 240 | collapseWhitespace: true, 241 | collapseBooleanAttributes: true, 242 | removeCommentsFromCDATA: true, 243 | removeOptionalTags: true 244 | }, 245 | files: [{ 246 | expand: true, 247 | cwd: '<%= yeoman.dist %>', 248 | src: ['*.html', 'views/{,*/}*.html'], 249 | dest: '<%= yeoman.dist %>' 250 | }] 251 | } 252 | }, 253 | 254 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 255 | // minsafe compatible so Uglify does not destroy the ng references 256 | ngmin: { 257 | dist: { 258 | files: [{ 259 | expand: true, 260 | cwd: '<%= yeoman.app %>/scripts', 261 | src: '**/*.js', 262 | dest: '<%= yeoman.dist %>/scripts' 263 | }] 264 | } 265 | }, 266 | 267 | // Replace Google CDN references 268 | cdnify: { 269 | dist: { 270 | html: ['<%= yeoman.dist %>/*.html'] 271 | } 272 | }, 273 | 274 | // Copies remaining files to places other tasks can use 275 | copy: { 276 | dist: { 277 | files: [{ 278 | expand: true, 279 | dot: true, 280 | cwd: '<%= yeoman.app %>', 281 | dest: '<%= yeoman.dist %>', 282 | src: [ 283 | '*.{ico,png,txt}', 284 | '.htaccess', 285 | '*.html', 286 | 'views/{,*/}*.html', 287 | 'bower_components/**/*', 288 | // 'images/{,*/}*.{webp}', 289 | 'images/**/*', 290 | 'fonts/*' 291 | ] 292 | }, { 293 | expand: true, 294 | cwd: '.tmp/images', 295 | dest: '<%= yeoman.dist %>/images', 296 | src: ['generated/*'] 297 | }] 298 | }, 299 | styles: { 300 | expand: true, 301 | cwd: '<%= yeoman.app %>/styles', 302 | dest: '.tmp/styles/', 303 | src: '{,*/}*.css' 304 | } 305 | }, 306 | 307 | // Run some tasks in parallel to speed up the build process 308 | concurrent: { 309 | server: [ 310 | 'compass:server' 311 | ], 312 | test: [ 313 | 'compass' 314 | ], 315 | dist: [ 316 | 'compass:dist', 317 | // 'imagemin', 318 | // 'svgmin' 319 | ] 320 | }, 321 | 322 | // By default, your `index.html`'s will take care of 323 | // minification. These next options are pre-configured if you do not wish 324 | // to use the Usemin blocks. 325 | cssmin: { 326 | dist: { 327 | files: { 328 | '<%= yeoman.dist %>/styles/main.css': [ 329 | '.tmp/styles/{,*/}*.css', 330 | '<%= yeoman.app %>/styles/{,*/}*.css' 331 | ] 332 | } 333 | } 334 | }, 335 | // uglify: { 336 | // dist: { 337 | // files: { 338 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 339 | // '<%= yeoman.dist %>/scripts/scripts.js' 340 | // ] 341 | // } 342 | // } 343 | // }, 344 | // concat: { 345 | // dist: {} 346 | // }, 347 | 348 | // Test settings 349 | karma: { 350 | unit: { 351 | configFile: 'karma.conf.js', 352 | singleRun: true 353 | } 354 | }, 355 | 356 | requirejs: { 357 | options: { 358 | baseUrl: '<%= yeoman.app %>/scripts', 359 | webroot: 'scripts', 360 | paths: { 361 | requireLib: '../bower_components/requirejs/require' 362 | }, 363 | out: '<%= yeoman.dist %>/scripts/app.js', 364 | name: 'main', 365 | mainConfigFile: '<%= yeoman.app %>/scripts/main.js' 366 | }, 367 | server: { 368 | options: { 369 | build: false 370 | } 371 | }, 372 | dist: { 373 | options: { 374 | // normalizeDirDefines: 'all', 375 | // onBuildRead: function (moduleName, path, contents) { 376 | // if(path == '/Users/mac/Projects/angular-music/client/dist/scripts/controllers/playerController.js') 377 | // console.info(require('ngmin').annotate(contents)); 378 | // return require('ngmin').annotate(contents); 379 | // } 380 | } 381 | } 382 | }, 383 | 384 | 'regex-replace': { 385 | css: { 386 | src: ['<%= yeoman.dist %>/styles/main.css'], 387 | actions: [ 388 | { 389 | } 390 | ] 391 | }, 392 | html: { 393 | src: ['<%= yeoman.dist %>/index.html'], 394 | actions: [ 395 | { 396 | name: 'requirejs-merge', 397 | search: '', 398 | replace: function(match){ 399 | return ''; 400 | }, 401 | flags: 'g' 402 | } 403 | ] 404 | } 405 | } 406 | }); 407 | 408 | 409 | grunt.registerTask('serve', function (target) { 410 | if (target === 'dist') { 411 | return grunt.task.run(['build', 'connect:dist:keepalive']); 412 | } 413 | 414 | grunt.task.run([ 415 | 'clean:server', 416 | 'bower-install', 417 | 'concurrent:server', 418 | 'autoprefixer', 419 | 'connect:livereload', 420 | 'watch' 421 | ]); 422 | }); 423 | 424 | grunt.registerTask('server', function () { 425 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 426 | grunt.task.run(['serve']); 427 | }); 428 | 429 | grunt.registerTask('test', [ 430 | 'clean:server', 431 | 'concurrent:test', 432 | 'autoprefixer', 433 | 'connect:test', 434 | 'karma' 435 | ]); 436 | 437 | grunt.registerTask('build', [ 438 | 'clean:dist', 439 | 'bower-install', 440 | 'useminPrepare', 441 | 'concurrent:dist', 442 | 'autoprefixer', 443 | 444 | 'concat', 445 | 446 | 'copy:dist', 447 | 'ngmin', 448 | 449 | 'cdnify', 450 | 'cssmin', 451 | // 'uglify', 452 | // 'rev', 453 | 454 | 'usemin', 455 | 'requirejs:dist', 456 | 'regex-replace:html', 457 | 'htmlmin' 458 | ]); 459 | 460 | grunt.registerTask('default', [ 461 | 'newer:jshint', 462 | 'test', 463 | 'build' 464 | ]); 465 | }; 466 | -------------------------------------------------------------------------------- /client/app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | --------------------------------------------------------------------------------