text: var parseFiles = function(files, cb) {
    var cnt = 0;
    var txts = [];
    var onload = function(res) {
      var txt = res.target.result;
      txts.push(txt.split(',')[1]);
      cnt--;
      if (!cnt) {
        cb(txts);
      }
    };
    _.each(files, function(file) {
      cnt++;
      console.log('starting file reader');
      var reader = new FileReader();
      reader.onload = onload;
      reader.onerror = function(err) {
        // return error
        // TODO: find a better way to propogate error upstream
        console.log('got back error', err);
        cb([]);
      };
      reader.readAsDataURL(file);
    });
  };
  
  angular
  .module('webui.ctrls.modal', [
    "ui.bootstrap", 'webui.services.deps', 'webui.services.modals', 'webui.services.rpc',
    'webui.services.configuration'
  ])
  .controller('ModalCtrl', [
    '$_', '$scope', '$modal', "$modals", '$rpc','$fileSettings', '$downloadProps',
    function(_, scope, $modal, modals, rpc, fsettings, dprops) {
  
    scope.getUris = {
      open: function(cb) {
        var self = this;
        this.uris = "";
        this.downloadSettingsCollapsed = true;
        this.advancedSettingsCollapsed = true;
        this.settings = {};
        this.fsettings = _.cloneDeep(fsettings);
        this.cb = cb;
  
        // fill in default download properties
        _.forEach(dprops, function(p) {
          self.settings[p] = self.fsettings[p];
          delete self.fsettings[p];
        });
  
        this.inst = $modal.open({
          templateUrl: "getUris.html",
          scope: scope,
          windowClass: "modal-large"
        });
        this.inst.result.then(function() {
          delete self.inst;
          if (self.cb) {
            var settings = {};
            // no need to send in default values, just the changed ones
            for (var i in self.settings) {
              if (fsettings[i].val != self.settings[i].val)
                  settings[i] = self.settings[i].val;
            }
            for (var i in self.fsettings) {
              if (fsettings[i].val != self.fsettings[i].val)
                  settings[i] = self.fsettings[i].val;
            }
  
            console.log('sending settings:', settings);
            self.cb(self.parse(), settings);
          }
        },
        function() {
          delete self.inst;
        });
      },
      parse: function() {
        return _
          .chain(this.uris.trim().split(/\r?\n/g))
          .map(function(d) {
            return _(d)
              .replace(/["'][^"']*["']/g, function(c) {
                return c.replace(/%/g,'%25').replace(/ /g,'%20');
              })
              .trim()
              .split(/\s+/g)
              .map(function(c) {
                return c.replace(/%20/g,' ').replace(/%25/g,'%').replace(/["']/g,'');
              });
          })
          .filter(function(d) { return d.length })
          .map(function(u){
                  /// transform bt-key into legal magnet link
                  /// support BASE32 encoding key string or HEX encoding key string
                  var str=_.toUpper(u[0])
                  var hashkey=_.head(_.split(str,"&"))//maybe url contains additional attribute  such as dn/tr
                  const base32Regex=/^[2-7 | A-Z]{32}$/
                  const hexRegex=/^[0-9 | A-Z]{40}$/
                  if(base32Regex.test(hashkey)||hexRegex.test(hashkey)){
                        u[0]= "magnet:?xt=urn:btih:"+u[0]
                        // if hashkey is legal bt-hashkey string then add protocol's head
                        // only support 32-bit length HEX encoding string or 40-bit length BASE32 length encoding string
                  }
                  return u
                })   
          .value();
      }
    };
  
    scope.settings = {
      open: function(settings, title, actionText, cb) {
        var self = this;
        this.settings = settings;
        this.title = title;
        this.actionText = actionText;
        this.inst = $modal.open({
          templateUrl: "settings.html",
          scope: scope,
          windowClass: "modal-large"
        });
        this.inst.result.then(function() {
          delete self.inst;
          if (cb) {
            cb(self.settings);
          }
        },
        function() {
          delete self.inst;
        });
      }
    };
  
    scope.selectFiles = {
      open: function(files, cb) {
        var self = this;
  
  	  this.files = _.cloneDeep(files);
        var groupFiles = function (files) {
            // sort files alphabetically
            files.sort(function (a, b) {
                if (a.relpath < b.relpath) {
                    return -1;
                } else {
                    return 1;
                }
            });
            function OrganizedFolder () {
                this.dirs = {};
                this.files = [];
                this.show = false;
                this.selected = true;
            }
            var folder = new OrganizedFolder(), tmp;
            for (var i = 0; i < files.length; i++) {
                tmp = folder;
                var str = files[i].relpath;
                var arr = str.split("/");
                for (var j = 0; j < arr.length - 1; j++) {
                    if (!tmp.dirs[arr[j]]) {
                        tmp.dirs[arr[j]] = new OrganizedFolder();
                    }
                    tmp = tmp.dirs[arr[j]];
                }
                tmp.files.push(files[i]);
            }
            return folder;
        };
        this.groupedFiles = groupFiles(this.files);
        this.inst = $modal.open({
          templateUrl: "selectFiles.html",
          scope: scope,
          windowClass: "modal-large"
        });
  
        this.inst.result.then(function() {
          delete self.inst;
  
          if (cb) {
            cb(self.files);
          }
        },
        function() {
          delete self.inst;
        });
      }
    };
  
    scope.connection = {
      open: function(defaults, cb) {
        var self = this;
  
        // XXX We need to actually clone this!
        this.conf = rpc.getConfiguration();
        this.inst = $modal.open({
          templateUrl: "connection.html",
          scope: scope,
          windowClass: "modal-large",
        });
  
        this.inst.result.then(function() {
          delete self.inst;
          if (cb) {
            cb(self.conf);
          }
        },
        function() {
          delete self.inst;
        });
      }
    };
  
    _.each(['getTorrents', 'getMetalinks'], function(name) {
      scope[name] = {
        open: function(cb) {
          var self = this;
          this.files = [];
          this.collapsed = true;
          this.settings = {};
          this.fsettings = _.cloneDeep(fsettings);
  
          // fill in default download properties
          _.forEach(dprops, function(p) {
            self.settings[p] = self.fsettings[p];
            delete self.fsettings[p];
          });
  
          this.inst = $modal.open({
            templateUrl: name + ".html",
            scope: scope,
            windowClass: "modal-large",
          });
  
          this.inst.result.then(function() {
            delete self.inst;
            if (cb) {
              parseFiles(self.files, function(txts) {
                var settings = {};
  
                // no need to send in default values, just the changed ones
                for (var i in self.settings) {
                  if (fsettings[i].val != self.settings[i].val)
                      settings[i] = self.settings[i].val;
                }
                for (var i in self.fsettings) {
                  if (fsettings[i].val != self.fsettings[i].val)
                      settings[i] = self.fsettings[i].val;
                }
  
                console.log('sending settings:', settings);
                cb(txts, settings);
              });
            }
          },
          function() {
            delete self.inst;
          });
        }
      };
    });
  
    _.each(["about", "server_info"], function(name) {
      scope[name] = {
        open: function() {
          var self = this;
          this.inst = $modal.open({
            templateUrl: name + ".html",
            scope: scope
          });
          this.inst.result.then(function() {
            delete self.inst;
          },
          function() {
            delete self.inst;
          });
        }
      };
    });
  
    rpc.once('getVersion', [], function(data) {
        scope.miscellaneous = data[0];
    });
  
    _.each([
      'getUris', 'getTorrents', 'getMetalinks', 'selectFiles',
      'settings', 'connection', 'server_info', 'about'
    ], function(name) {
      modals.register(name, function() {
        if (scope[name].inst) {
          // Already open.
          return;
        }
        var args = Array.prototype.slice.call(arguments, 0);
        scope[name].open.apply(scope[name], args);
      });
    });
  
  }]);
  