2013-11-03 00:51:28 +00:00
define ( [
"settings!" ,
"command" ,
"sessions" ,
"file" ,
2013-11-04 01:15:34 +00:00
"manos" ,
2013-11-06 20:02:20 +00:00
"dialog" ,
2013-11-07 05:56:12 +00:00
"contextMenus" ,
2013-11-03 00:51:28 +00:00
"dom2"
2013-11-07 05:56:12 +00:00
] , function ( Settings , command , sessions , File , M , dialog , context ) {
2013-11-04 01:15:34 +00:00
/ *
It 's tempting to store projects in local storage, similar to the way that we retain files for tabs, but this would be a mistake. Reading from storage is a pain, because it wants to store a single level deep, and we' ll want to alter parts of the setup individually .
Instead , we 'll retain a single file handle to the project file, which (as JSON) will store the IDs of individual directories, the project-specific settings, and (hopefully, one day) build systems. This also gets us around the issues of restored directory order and constantly updating the retained file list--we' ll just update it when the project file is saved .
* /
2013-11-03 00:51:28 +00:00
var guidCounter = 0 ;
2013-11-04 16:41:22 +00:00
//FSNodes are used to track filesystem state inside projects
2013-11-03 00:51:28 +00:00
var FSNode = function ( entry ) {
this . children = [ ] ;
this . id = guidCounter ++ ;
if ( entry ) this . setEntry ( entry ) ;
} ;
FSNode . prototype = {
isDirectory : false ,
entry : null ,
tab : null ,
id : null ,
label : null ,
setEntry : function ( entry , c ) {
this . entry = entry ;
this . label = entry . name ;
this . isDirectory = entry . isDirectory ;
} ,
walk : function ( done ) {
var self = this ;
var entries = [ ] ;
var reader = this . entry . createReader ( ) ;
var inc = 1 ;
var check = function ( ) {
inc -- ;
if ( inc == 0 ) {
return done ( self ) ;
}
} ;
var collect = function ( list ) {
if ( list . length == 0 ) return complete ( ) ;
entries . push . apply ( entries , list ) ;
reader . readEntries ( collect ) ;
} ;
var complete = function ( ) {
2013-11-07 05:56:12 +00:00
self . children = [ ] ;
2013-11-03 00:51:28 +00:00
for ( var i = 0 ; i < entries . length ; i ++ ) {
var entry = entries [ i ] ;
if ( entry . name [ 0 ] == "." && entry . isDirectory ) continue ;
var node = new FSNode ( entry ) ;
self . children . push ( node ) ;
if ( node . isDirectory ) {
inc ++ ;
node . walk ( check ) ;
}
}
check ( ) ;
} ;
reader . readEntries ( collect ) ;
}
} ;
2013-11-06 01:44:19 +00:00
var ProjectManager = function ( element ) {
2013-11-03 00:51:28 +00:00
this . directories = [ ] ;
2013-11-06 01:44:19 +00:00
this . pathMap = { } ;
2013-11-03 02:29:10 +00:00
this . expanded = { } ;
2013-11-06 02:46:16 +00:00
this . project = null ;
2013-11-06 01:44:19 +00:00
this . projectFile = null ;
if ( element ) {
this . setElement ( element )
}
2013-11-06 02:46:16 +00:00
var self = this ;
chrome . storage . local . get ( "retainedProject" , function ( data ) {
if ( data . retainedProject ) {
2013-11-07 18:24:50 +00:00
var file = new File ( ) ;
file . onWrite = self . watchProjectFile . bind ( self ) ;
file . restore ( data . retainedProject , function ( err , f ) {
2013-11-06 02:46:16 +00:00
if ( err ) {
2013-11-06 02:52:00 +00:00
return chrome . storage . local . remove ( "retainedProject" ) ;
2013-11-06 02:46:16 +00:00
}
file . read ( function ( err , data ) {
if ( err ) return ;
2013-11-07 18:24:50 +00:00
self . projectFile = file ;
2013-11-06 02:46:16 +00:00
self . loadProject ( JSON . parse ( data ) ) ;
} ) ;
} ) ;
}
} )
2013-11-03 00:51:28 +00:00
} ;
ProjectManager . prototype = {
element : null ,
addDirectory : function ( c ) {
var self = this ;
chrome . fileSystem . chooseEntry ( { type : "openDirectory" } , function ( d ) {
var root = new FSNode ( d ) ;
self . directories . push ( root ) ;
2013-11-03 02:29:10 +00:00
root . walk ( self . render . bind ( self ) ) ;
2013-11-03 00:51:28 +00:00
} ) ;
} ,
2013-11-07 05:56:12 +00:00
removeDirectory : function ( args ) {
this . directories = this . directories . filter ( function ( node ) {
return node . id != args . id ;
} ) ;
this . render ( ) ;
} ,
2013-11-04 16:41:22 +00:00
removeAllDirectories : function ( ) {
this . directories = [ ] ;
this . render ( ) ;
} ,
refresh : function ( ) {
var counter = 0 ;
var self = this ;
var check = function ( ) {
counter ++ ;
if ( counter = self . directories . length ) {
self . render ( ) ;
}
}
this . directories . forEach ( function ( d ) {
d . walk ( check ) ;
} ) ;
} ,
2013-11-03 00:51:28 +00:00
render : function ( ) {
2013-11-03 02:29:10 +00:00
if ( ! this . element ) return ;
this . element . innerHTML = "" ;
2013-11-04 00:28:17 +00:00
if ( this . directories . length == 0 ) {
2013-11-04 16:53:15 +00:00
this . element . removeClass ( "show" ) ;
2013-11-04 00:28:17 +00:00
return ;
}
2013-11-04 16:53:15 +00:00
var self = this ;
this . element . addClass ( "show" ) ;
2013-11-06 01:44:19 +00:00
this . pathMap = { } ;
2013-11-08 01:04:50 +00:00
var walker = function ( node ) {
2013-11-04 00:28:17 +00:00
var li = document . createElement ( "li" ) ;
2013-11-06 17:17:05 +00:00
var a = document . createElement ( "a" ) ;
li . append ( a ) ;
2013-11-04 00:28:17 +00:00
if ( node . isDirectory ) {
2013-11-06 17:17:05 +00:00
a . innerHTML = node . label ;
a . setAttribute ( "data-full-path" , node . entry . fullPath ) ;
a . addClass ( "directory" ) ;
2013-11-08 01:04:50 +00:00
if ( self . directories . indexOf ( node ) != - 1 ) {
2013-11-08 01:25:31 +00:00
a . href = context . makeURL ( "root/directory" , node . id ) ;
2013-11-07 05:56:12 +00:00
} else {
a . href = context . makeURL ( "directory" , node . id ) ;
}
a . setAttribute ( "command" , null ) ;
2013-11-04 16:53:15 +00:00
if ( self . expanded [ node . entry . fullPath ] ) {
li . addClass ( "expanded" ) ;
}
2013-11-04 00:28:17 +00:00
var ul = document . createElement ( "ul" ) ;
2013-11-06 01:44:19 +00:00
node . children . sort ( function ( a , b ) {
if ( a . isDirectory != b . isDirectory ) {
//sneaky casting trick
2013-11-08 01:25:31 +00:00
return ~ ~ b . isDirectory - ~ ~ a . isDirectory ;
2013-11-06 01:44:19 +00:00
}
if ( a . label < b . label ) return - 1 ;
if ( a . label > b . label ) return 1 ;
return 0 ;
} ) ;
2013-11-04 00:28:17 +00:00
for ( var i = 0 ; i < node . children . length ; i ++ ) {
2013-11-08 01:04:50 +00:00
ul . append ( walker ( node . children [ i ] ) ) ;
2013-11-04 00:28:17 +00:00
}
2013-11-04 16:53:15 +00:00
li . append ( ul ) ;
2013-11-04 16:41:22 +00:00
} else {
2013-11-06 01:44:19 +00:00
var path = node . entry . fullPath ;
2013-11-04 16:53:15 +00:00
a . innerHTML = node . label ;
2013-11-08 01:32:38 +00:00
a . href = context . makeURL ( "file" , node . entry . fullPath . replace ( /[\/\\]/g , "@" ) ) ;
2013-11-06 01:44:19 +00:00
a . setAttribute ( "argument" , path ) ;
a . setAttribute ( "command" , "project:open-file" ) ;
self . pathMap [ path ] = node ;
2013-11-04 00:28:17 +00:00
}
return li ;
} ;
var trees = this . directories . map ( walker ) ;
var list = document . createElement ( "ul" ) ;
trees . forEach ( function ( dir ) {
dir . classList . add ( "expanded" ) ;
list . appendChild ( dir ) ;
} ) ;
this . element . appendChild ( list ) ;
2013-11-03 01:30:37 +00:00
} ,
2013-11-04 16:41:22 +00:00
setElement : function ( el ) {
this . element = el ;
this . bindEvents ( ) ;
} ,
2013-11-03 01:30:37 +00:00
bindEvents : function ( ) {
2013-11-04 16:53:15 +00:00
var self = this ;
2013-11-04 16:41:22 +00:00
this . element . on ( "click" , function ( e ) {
var target = e . target ;
2013-11-04 16:53:15 +00:00
if ( target . hasClass ( "directory" ) ) {
2013-11-06 17:17:05 +00:00
target . parentElement . toggle ( "expanded" ) ;
2013-11-04 16:53:15 +00:00
var path = target . getAttribute ( "data-full-path" ) ;
self . expanded [ path ] = ! ! ! self . expanded [ path ] ;
}
2013-11-04 16:41:22 +00:00
} ) ;
2013-11-03 01:30:37 +00:00
} ,
2013-11-06 01:44:19 +00:00
openFile : function ( path ) {
var self = this ;
2013-11-06 16:05:13 +00:00
var found = false ;
var node = this . pathMap [ path ] ;
if ( ! node ) return ;
2013-11-06 01:44:19 +00:00
//walk through existing tabs to see if it's already open
var tabs = sessions . getAllTabs ( ) ;
2013-11-06 16:05:13 +00:00
chrome . fileSystem . getDisplayPath ( node . entry , function ( path ) {
//look through the tabs for matching display paths
M . map (
tabs ,
function ( tab , i , c ) {
if ( ! tab . file || tab . file . virtual ) {
return c ( false ) ;
}
tab . file . getPath ( function ( p ) {
if ( p == path ) {
sessions . setCurrent ( tab ) ;
found = true ;
}
//we don't actually use the result
c ( ) ;
} ) ;
} ,
//if no match found, create a tab
function ( ) {
if ( found ) return ;
var file = new File ( node . entry ) ;
file . read ( function ( err , data ) {
sessions . addFile ( data , file ) ;
} )
}
) ;
2013-11-06 01:44:19 +00:00
} ) ;
2013-11-04 00:28:17 +00:00
} ,
2013-11-06 02:46:16 +00:00
generateProject : function ( ) {
var project = this . project || { } ;
//everything but "folders" is left as-is
//run through all directories, retain them, and add to the structure
project . folders = this . directories . map ( function ( node ) {
var id = chrome . fileSystem . retainEntry ( node . entry ) ;
return {
retained : id ,
path : node . entry . fullPath
} ;
} ) ;
var json = JSON . stringify ( project , null , 2 ) ;
if ( this . projectFile ) {
this . projectFile . write ( json ) ;
} else {
2013-11-07 18:24:50 +00:00
var file = new File ( ) ;
2013-11-06 20:02:20 +00:00
var watch = this . watchProjectFile . bind ( this ) ;
2013-11-07 18:24:50 +00:00
var self = this ;
2013-11-06 02:46:16 +00:00
file . open ( "save" , function ( ) {
file . write ( json ) ;
2013-11-06 16:12:00 +00:00
var id = file . retain ( ) ;
2013-11-06 02:46:16 +00:00
chrome . storage . local . set ( { retainedProject : id } ) ;
2013-11-06 20:02:20 +00:00
file . onWrite = watch ;
2013-11-07 18:24:50 +00:00
self . projectFile = file ;
2013-11-06 02:46:16 +00:00
} ) ;
}
return json ;
2013-11-04 16:41:22 +00:00
} ,
2013-11-06 02:46:16 +00:00
openProjectFile : function ( ) {
2013-11-07 18:24:50 +00:00
var file = new File ( ) ;
2013-11-06 16:12:00 +00:00
var self = this ;
file . open ( function ( ) {
file . read ( function ( err , data ) {
if ( err ) return ;
self . loadProject ( data ) ;
var id = file . retain ( ) ;
chrome . storage . local . set ( { retainedProject : id } ) ;
2013-11-07 18:24:50 +00:00
self . projectFile = file ;
2013-11-06 16:12:00 +00:00
} ) ;
2013-11-06 20:02:20 +00:00
file . onWrite = self . watchProjectFile ;
} ) ;
} ,
watchProjectFile : function ( ) {
var self = this ;
this . projectFile . read ( function ( err , data ) {
if ( err ) return ;
self . loadProject ( data ) ;
2013-11-06 16:12:00 +00:00
} ) ;
2013-11-04 16:41:22 +00:00
} ,
2013-11-06 02:46:16 +00:00
loadProject : function ( project ) {
var self = this ;
2013-11-04 16:41:22 +00:00
//project is the JSON from a project file
2013-11-06 02:46:16 +00:00
if ( typeof project == "string" ) {
project = JSON . parse ( project ) ;
}
2013-11-06 20:02:20 +00:00
this . project = project ;
2013-11-04 16:41:22 +00:00
//assign settings
2013-11-06 16:35:57 +00:00
if ( project . settings ) {
Settings . setProject ( project . settings ) ;
}
2013-11-04 16:41:22 +00:00
//restore directory entries that can be restored
2013-11-06 20:02:20 +00:00
this . directories = [ ] ;
2013-11-06 02:46:16 +00:00
M . map (
project . folders ,
function ( folder , index , c ) {
chrome . fileSystem . restoreEntry ( folder . retained , function ( entry ) {
2013-11-06 20:18:50 +00:00
//remember, you can only restore project directories you'd previously opened
if ( ! entry ) return c ( ) ;
2013-11-06 02:46:16 +00:00
var node = new FSNode ( entry ) ;
self . directories . push ( node ) ;
node . walk ( c ) ;
} ) ;
} ,
function ( ) {
self . render ( ) ;
}
) ;
2013-11-06 02:52:00 +00:00
} ,
2013-11-06 20:02:20 +00:00
editProjectFile : function ( ) {
if ( ! this . projectFile ) {
return dialog ( "No project opened." ) ;
}
var self = this ;
this . projectFile . read ( function ( err , data ) {
if ( err ) return ;
sessions . addFile ( data , self . projectFile ) ;
} ) ;
} ,
2013-11-06 17:17:05 +00:00
clearProject : function ( keepRetained ) {
2013-11-06 02:52:00 +00:00
this . projectFile = null ;
this . directories = [ ] ;
this . project = { } ;
2013-11-06 16:35:57 +00:00
Settings . clearProject ( ) ;
2013-11-06 17:17:05 +00:00
if ( ! keepRetained ) chrome . storage . local . remove ( "retainedProject" ) ;
2013-11-06 02:52:00 +00:00
this . render ( ) ;
2013-11-03 00:51:28 +00:00
}
} ;
2013-11-06 01:44:19 +00:00
var pm = new ProjectManager ( document . find ( ".project" ) ) ;
2013-11-03 02:29:10 +00:00
command . on ( "project:add-dir" , pm . addDirectory . bind ( pm ) ) ;
2013-11-04 00:28:17 +00:00
command . on ( "project:remove-all" , pm . removeAllDirectories . bind ( pm ) ) ;
2013-11-06 02:46:16 +00:00
command . on ( "project:generate" , pm . generateProject . bind ( pm ) ) ;
2013-11-03 01:30:37 +00:00
command . on ( "project:open-file" , pm . openFile . bind ( pm ) ) ;
2013-11-04 16:41:22 +00:00
command . on ( "project:refresh-dir" , pm . refresh . bind ( pm ) ) ;
2013-11-06 02:52:00 +00:00
command . on ( "project:open" , pm . openProjectFile . bind ( pm ) ) ;
2013-11-06 20:02:20 +00:00
command . on ( "project:edit" , pm . editProjectFile . bind ( pm ) ) ;
2013-11-06 02:52:00 +00:00
command . on ( "project:clear" , pm . clearProject . bind ( pm ) ) ;
2013-11-07 05:56:12 +00:00
2013-11-08 01:25:31 +00:00
context . register ( "Remove from Project" , "removeDirectory" , "root/directory/:id" , pm . removeDirectory . bind ( pm ) ) ;
2013-11-03 00:51:28 +00:00
} ) ;