// ===================================================================================================
//                           _  __     _ _
//                          | |/ /__ _| | |_ _  _ _ _ __ _
//                          | ' </ _` | |  _| || | '_/ _` |
//                          |_|\_\__,_|_|\__|\_,_|_| \__,_|
//
// This file is part of the Kaltura Collaborative Media Suite which allows users
// to do with audio, video, and animation what Wiki platfroms allow them to do with
// text.
//
// Copyright (C) 2006-2011  Kaltura Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// @ignore
// ===================================================================================================
package com.kaltura.delegates {

	import com.kaltura.config.IKalturaConfig;
	import com.kaltura.config.KalturaConfig;
	import com.kaltura.core.KClassFactory;
	import com.kaltura.encryption.MD5;
	import com.kaltura.errors.KalturaError;
	import com.kaltura.events.KalturaEvent;
	import com.kaltura.net.KalturaCall;

	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.FileReference;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.utils.Timer;
	import flash.utils.getDefinitionByName;

	public class WebDelegateBase extends EventDispatcher implements IKalturaCallDelegate {

		public static var CONNECT_TIME:int = 120000; //120 secs
		public static var LOAD_TIME:int = 120000; //120 secs

		protected var connectTimer:Timer;
		protected var loadTimer:Timer;

		protected var _call:KalturaCall;
		protected var _config:KalturaConfig;

		protected var loader:URLLoader;
		protected var fileRef:FileReference;

		//Setters & getters
		public function get call():KalturaCall {
			return _call;
		}

		public function set call(newVal:KalturaCall):void {
			_call = newVal;
		}

		public function get config():IKalturaConfig {
			return _config;
		}

		public function set config(newVal:IKalturaConfig):void {
			_config = newVal as KalturaConfig;
		}

		public function WebDelegateBase(call:KalturaCall = null, config:KalturaConfig = null) {
			this.call = call;
			this.config = config;
			if (!call)
				return; //maybe a multi request
			if (call.useTimeout) {
				connectTimer = new Timer(CONNECT_TIME, 1);
				connectTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onConnectTimeout);

				loadTimer = new Timer(LOAD_TIME, 1);
				loadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onLoadTimeOut);
			}
			execute();
		}

		public function close():void {
			try {
				loader.close();
			}
			catch (e:*) {
			}

			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.stop();
			}
		}

		protected function onConnectTimeout(event:TimerEvent):void {
			var kError:KalturaError = new KalturaError();
			kError.errorCode = "CONNECTION_TIMEOUT";
			kError.errorMsg = "Connection Timeout: " + CONNECT_TIME / 1000 + " sec with no post command from kaltura client.";
			_call.handleError(kError);
			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));

			loadTimer.stop();
			close();
		}

		protected function onLoadTimeOut(event:TimerEvent):void {
			connectTimer.stop();

			close();

			var kError:KalturaError = new KalturaError();
			kError.errorCode = "POST_TIMEOUT";
			kError.errorMsg = "Post Timeout: " + LOAD_TIME / 1000 + " sec with no post result.";
			_call.handleError(kError);
			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
		}

		protected function execute():void {
			if (call == null) {
				throw new Error('No call defined.');
			}
			post(); //post the call
		}

		/**
		* Helper function for sending the call straight to the server
		*/
		protected function post():void {

			addOptionalArguments();

			formatRequest();

			sendRequest();

			if (call.useTimeout) {
				connectTimer.start();
			}
		}

		protected function formatRequest():void {
			//The configuration is stronger then the args
			if (_config.partnerId != null && _call.args["partnerId"] == -1)
				_call.setRequestArgument("partnerId", _config.partnerId);

			if (_config.ks != null)
				_call.setRequestArgument("ks", _config.ks);

			if (_config.clientTag != null)
				_call.setRequestArgument("clientTag", _config.clientTag);

			_call.setRequestArgument("ignoreNull", _config.ignoreNull);

			//Create signature hash.
			//call.setRequestArgument("kalsig", getMD5Checksum(call));
		}

		protected function getMD5Checksum(call:KalturaCall):String {
			var props:Array = new Array();
			for each (var prop:String in call.args)
				props.push(prop);

			props.push("service");
			props.push("action");
			props.sort();

			var s:String;
			for each (prop in props) {
				s += prop;

				if (prop == "service")
					s += call.service;
				else if (prop == "action")
					s += call.action;
				else
					s += call.args[prop];
			}

			return MD5.encrypt(s);
		}

		protected function sendRequest():void {

			//construct the loader
			createURLLoader();

			//Create signature hash.
			var kalsig:String = getMD5Checksum(call);

			//create the service request for normal calls
			var url:String = _config.protocol + _config.domain + "/" + _config.srvUrl + "?service=" + call.service + "&action=" + call.action + "&kalsig=" + kalsig;;

			if (_call.method == URLRequestMethod.GET)
				url += "&";

			var req:URLRequest = new URLRequest(url);
			req.contentType = "application/x-www-form-urlencoded";
			req.method = call.method;
			req.data = call.args;

			loader.dataFormat = URLLoaderDataFormat.TEXT;
			loader.load(req);
		}

		protected function createURLLoader():void {
			loader = new URLLoader();
			loader.addEventListener(Event.COMPLETE, onDataComplete);
			loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onHTTPStatus);
			loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
			loader.addEventListener(Event.OPEN, onOpen);
		}

		protected function onHTTPStatus(event:HTTPStatusEvent):void {
		}

		protected function onOpen(event:Event):void {
			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.start();
			}
		}

		protected function addOptionalArguments():void {
			//add optional args here
		}

		// Event Handlers

		/**
		 * try to process received data.
		 * if procesing failed, let call handle the processing error
		 * @param event load complete event
		 */
		protected function onDataComplete(event:Event):void {
			try {
				handleResult(XML(event.target.data));
			}
			catch (e:Error) {
				var kErr:KalturaError = new KalturaError();
				kErr.errorCode = String(e.errorID);
				kErr.errorMsg = e.message;
				_call.handleError(kErr);
			}
		}

		/**
		 * handle io or security error events from the loader.
		 * create relevant KalturaError, let the call process it.
		 * @param event
		 */
		protected function onError(event:ErrorEvent):void {
			clean();
			var kError:KalturaError = createKalturaError(event, loader.data);

			if (!kError) {
				kError = new KalturaError();
				kError.errorMsg = event.text;
				//kError.errorCode;
			}

			call.handleError(kError);

			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
		}

		/**
		 * parse the server's response and let the call process it.
		 * @param result  server's response
		 */
		protected function handleResult(result:XML):void {
			clean();

			var error:KalturaError = validateKalturaResponse(result);

			if (error == null) {
				var digestedResult:Object = parse(result);
				call.handleResult(digestedResult);
			}
			else {
				call.handleError(error);
			}
		}

		/**
		* stop timers and clean event listeners
		 */
		protected function clean():void {
			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.stop();
			}

			if (loader == null) {
				return;
			}

			loader.removeEventListener(Event.COMPLETE, onDataComplete);
			loader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
			loader.removeEventListener(Event.OPEN, onOpen);
		}

		/**
		* create the correct object and populate it with the given values. if the needed class is not found
		 * in the file, a generic object is created with attributes matching the XML attributes.
		* Override this parssing function in the specific delegate to create the correct object.
		* @param   result      instance attributes
		* @return an instance of the class declared by the given XML.
		* */
		public function parse(result:XML):* {
			//by defualt create the response object
			var cls:Class;
			try {
				cls = getDefinitionByName('com.kaltura.vo.' + result.result.objectType) as Class;
			}
			catch (e:Error) {
				cls = Object;
			}
			var obj:* = (new KClassFactory(cls)).newInstanceFromXML(result.result);
			return obj;
		}

		/**
		 * If the result string holds an error, return a KalturaError object with
		 * relevant values. <br/>
		 * Overide this to create validation object and fill it.
		 * @param result  the string returned from the server.
		 * @return  matching error object
		 */
		protected function validateKalturaResponse(result:String):KalturaError {
			var kError:KalturaError = null;
			var xml:XML = XML(result);
			if (xml.result.hasOwnProperty('error')
				&& xml.result.error.hasOwnProperty('code')
				&& xml.result.error.hasOwnProperty('message')) {

				kError = new KalturaError();
				kError.errorCode = String(xml.result.error.code);
				kError.errorMsg = xml.result.error.message;
				dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
			}

			return kError;
		}

		/**
		 * create error object and fill it with relevant details
		 * @param event
		 * @param loaderData
		 * @return detailed KalturaError to be processed
		 */
		protected function createKalturaError(event:ErrorEvent, loaderData:*):KalturaError {
			var ke:KalturaError = new KalturaError();
			ke.errorMsg = event.text;
			return ke;
		}

		/**
		* create the url that is used for serve actions
		* @param call    the KalturaCall that defines the required parameters
		 * @return URLRequest with relevant parameters
		* */
		public function getServeUrl(call:KalturaCall):URLRequest {
			var url:String = _config.protocol + _config.domain + "/" + _config.srvUrl + "?service=" + call.service + "&action=" + call.action;
			for (var key:String in call.args) {
				url += "&" + key + "=" + call.args[key];
			}
			var req:URLRequest = new URLRequest(url);
			req.contentType = "application/x-www-form-urlencoded";
			req.method = call.method;
//          req.data = call.args;
			return req;
		}

	}
}

// ===================================================================================================
//                           _  __     _ _
//                          | |/ /__ _| | |_ _  _ _ _ __ _
//                          | ' </ _` | |  _| || | '_/ _` |
//                          |_|\_\__,_|_|\__|\_,_|_| \__,_|
//
// This file is part of the Kaltura Collaborative Media Suite which allows users
// to do with audio, video, and animation what Wiki platfroms allow them to do with
// text.
//
// Copyright (C) 2006-2011  Kaltura Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// @ignore
// ===================================================================================================
package com.kaltura.delegates {

	import com.kaltura.config.IKalturaConfig;
	import com.kaltura.config.KalturaConfig;
	import com.kaltura.core.KClassFactory;
	import com.kaltura.encryption.MD5;
	import com.kaltura.errors.KalturaError;
	import com.kaltura.events.KalturaEvent;
	import com.kaltura.net.KalturaCall;

	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.FileReference;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.utils.Timer;
	import flash.utils.getDefinitionByName;

	public class WebDelegateBase extends EventDispatcher implements IKalturaCallDelegate {

		public static var CONNECT_TIME:int = 120000; //120 secs
		public static var LOAD_TIME:int = 120000; //120 secs

		protected var connectTimer:Timer;
		protected var loadTimer:Timer;

		protected var _call:KalturaCall;
		protected var _config:KalturaConfig;

		protected var loader:URLLoader;
		protected var fileRef:FileReference;

		//Setters & getters
		public function get call():KalturaCall {
			return _call;
		}

		public function set call(newVal:KalturaCall):void {
			_call = newVal;
		}

		public function get config():IKalturaConfig {
			return _config;
		}

		public function set config(newVal:IKalturaConfig):void {
			_config = newVal as KalturaConfig;
		}

		public function WebDelegateBase(call:KalturaCall = null, config:KalturaConfig = null) {
			this.call = call;
			this.config = config;
			if (!call)
				return; //maybe a multi request
			if (call.useTimeout) {
				connectTimer = new Timer(CONNECT_TIME, 1);
				connectTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onConnectTimeout);

				loadTimer = new Timer(LOAD_TIME, 1);
				loadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onLoadTimeOut);
			}
			execute();
		}

		public function close():void {
			try {
				loader.close();
			}
			catch (e:*) {
			}

			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.stop();
			}
		}

		protected function onConnectTimeout(event:TimerEvent):void {
			var kError:KalturaError = new KalturaError();
			kError.errorCode = "CONNECTION_TIMEOUT";
			kError.errorMsg = "Connection Timeout: " + CONNECT_TIME / 1000 + " sec with no post command from kaltura client.";
			_call.handleError(kError);
			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));

			loadTimer.stop();
			close();
		}

		protected function onLoadTimeOut(event:TimerEvent):void {
			connectTimer.stop();

			close();

			var kError:KalturaError = new KalturaError();
			kError.errorCode = "POST_TIMEOUT";
			kError.errorMsg = "Post Timeout: " + LOAD_TIME / 1000 + " sec with no post result.";
			_call.handleError(kError);
			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
		}

		protected function execute():void {
			if (call == null) {
				throw new Error('No call defined.');
			}
			post(); //post the call
		}

		/**
		* Helper function for sending the call straight to the server
		*/
		protected function post():void {

			addOptionalArguments();

			formatRequest();

			sendRequest();

			if (call.useTimeout) {
				connectTimer.start();
			}
		}

		protected function formatRequest():void {
			//The configuration is stronger then the args
			if (_config.partnerId != null && _call.args["partnerId"] == -1)
				_call.setRequestArgument("partnerId", _config.partnerId);

			if (_config.ks != null)
				_call.setRequestArgument("ks", _config.ks);

			if (_config.clientTag != null)
				_call.setRequestArgument("clientTag", _config.clientTag);

			_call.setRequestArgument("ignoreNull", _config.ignoreNull);

			//Create signature hash.
			//call.setRequestArgument("kalsig", getMD5Checksum(call));
		}

		protected function getMD5Checksum(call:KalturaCall):String {
			var props:Array = new Array();
			for each (var prop:String in call.args)
				props.push(prop);

			props.push("service");
			props.push("action");
			props.sort();

			var s:String;
			for each (prop in props) {
				s += prop;

				if (prop == "service")
					s += call.service;
				else if (prop == "action")
					s += call.action;
				else
					s += call.args[prop];
			}

			return MD5.encrypt(s);
		}

		protected function sendRequest():void {

			//construct the loader
			createURLLoader();

			//Create signature hash.
			var kalsig:String = getMD5Checksum(call);

			//create the service request for normal calls
			var url:String = _config.protocol + _config.domain + "/" + _config.srvUrl + "?service=" + call.service + "&action=" + call.action + "&kalsig=" + kalsig;;

			if (_call.method == URLRequestMethod.GET)
				url += "&";

			var req:URLRequest = new URLRequest(url);
			req.contentType = "application/x-www-form-urlencoded";
			req.method = call.method;
			req.data = call.args;

			loader.dataFormat = URLLoaderDataFormat.TEXT;
			loader.load(req);
		}

		protected function createURLLoader():void {
			loader = new URLLoader();
			loader.addEventListener(Event.COMPLETE, onDataComplete);
			loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onHTTPStatus);
			loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
			loader.addEventListener(Event.OPEN, onOpen);
		}

		protected function onHTTPStatus(event:HTTPStatusEvent):void {
		}

		protected function onOpen(event:Event):void {
			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.start();
			}
		}

		protected function addOptionalArguments():void {
			//add optional args here
		}

		// Event Handlers

		/**
		 * try to process received data.
		 * if procesing failed, let call handle the processing error
		 * @param event load complete event
		 */
		protected function onDataComplete(event:Event):void {
			try {
				handleResult(XML(event.target.data));
			}
			catch (e:Error) {
				var kErr:KalturaError = new KalturaError();
				kErr.errorCode = String(e.errorID);
				kErr.errorMsg = e.message;
				_call.handleError(kErr);
			}
		}

		/**
		 * handle io or security error events from the loader.
		 * create relevant KalturaError, let the call process it.
		 * @param event
		 */
		protected function onError(event:ErrorEvent):void {
			clean();
			var kError:KalturaError = createKalturaError(event, loader.data);

			if (!kError) {
				kError = new KalturaError();
				kError.errorMsg = event.text;
				kError.errorCode = event.type; 	// either IOErrorEvent.IO_ERROR or SecurityErrorEvent.SECURITY_ERROR
			}

			call.handleError(kError);

			dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
		}

		/**
		 * parse the server's response and let the call process it.
		 * @param result  server's response
		 */
		protected function handleResult(result:XML):void {
			clean();

			var error:KalturaError = validateKalturaResponse(result);

			if (error == null) {
				var digestedResult:Object = parse(result);
				call.handleResult(digestedResult);
			}
			else {
				call.handleError(error);
			}
		}

		/**
		* stop timers and clean event listeners
		 */
		protected function clean():void {
			if (call.useTimeout) {
				connectTimer.stop();
				loadTimer.stop();
			}

			if (loader == null) {
				return;
			}

			loader.removeEventListener(Event.COMPLETE, onDataComplete);
			loader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
			loader.removeEventListener(Event.OPEN, onOpen);
		}

		/**
		* create the correct object and populate it with the given values. if the needed class is not found
		 * in the file, a generic object is created with attributes matching the XML attributes.
		* Override this parssing function in the specific delegate to create the correct object.
		* @param   result      instance attributes
		* @return an instance of the class declared by the given XML.
		* */
		public function parse(result:XML):* {
			//by defualt create the response object
			var cls:Class;
			try {
				cls = getDefinitionByName('com.kaltura.vo.' + result.result.objectType) as Class;
			}
			catch (e:Error) {
				cls = Object;
			}
			var obj:* = (new KClassFactory(cls)).newInstanceFromXML(result.result);
			return obj;
		}

		/**
		 * If the result string holds an error, return a KalturaError object with
		 * relevant values. <br/>
		 * Overide this to create validation object and fill it.
		 * @param result  the string returned from the server.
		 * @return  matching error object
		 */
		protected function validateKalturaResponse(result:String):KalturaError {
			var kError:KalturaError = null;
			var xml:XML = XML(result);
			if (xml.result.hasOwnProperty('error')
				&& xml.result.error.hasOwnProperty('code')
				&& xml.result.error.hasOwnProperty('message')) {

				kError = new KalturaError();
				kError.errorCode = String(xml.result.error.code);
				kError.errorMsg = xml.result.error.message;
				dispatchEvent(new KalturaEvent(KalturaEvent.FAILED, false, false, false, null, kError));
			}

			return kError;
		}

		/**
		 * create error object and fill it with relevant details
		 * @param event
		 * @param loaderData
		 * @return detailed KalturaError to be processed
		 */
		protected function createKalturaError(event:ErrorEvent, loaderData:*):KalturaError {
			var ke:KalturaError = new KalturaError();
			ke.errorMsg = event.text;
			return ke;
		}

		/**
		* create the url that is used for serve actions
		* @param call    the KalturaCall that defines the required parameters
		 * @return URLRequest with relevant parameters
		* */
		public function getServeUrl(call:KalturaCall):URLRequest {
			var url:String = _config.protocol + _config.domain + "/" + _config.srvUrl + "?service=" + call.service + "&action=" + call.action;
			for (var key:String in call.args) {
				url += "&" + key + "=" + call.args[key];
			}
			var req:URLRequest = new URLRequest(url);
			req.contentType = "application/x-www-form-urlencoded";
			req.method = call.method;
//          req.data = call.args;
			return req;
		}

	}
}

fwr: add error code (event.type) to ioerror / securityerror events

git-svn-id: 8a2ccb88241e16c78017770bc38d91d6d5396a5a@96549 6b8eccd3-e8c5-4e7d-8186-e12b5326b719


package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.events.Event;
    import flash.utils.setTimeout;

    import FacilitatorSocket;
    import events.FacilitatorSocketEvent;

    import ProxyPair;
    import RTMFPProxyPair;
    import SQSProxyPair;
    import TCPProxyPair;

    import rtmfp.CirrusSocket;
    import rtmfp.events.CirrusSocketEvent;

    public class swfcat extends Sprite
    {
        /* Adobe's Cirrus server for RTMFP connections.
           The Cirrus key is defined at compile time by
           reading from the CIRRUS_KEY environment var. */
        private const DEFAULT_CIRRUS_ADDR:String = "rtmfp://p2p.rtmfp.net";
        private const DEFAULT_CIRRUS_KEY:String = RTMFP::CIRRUS_KEY;

        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "tor-facilitator.bamsoftware.com",
            port: 9002
        };

        /* Default Tor client to use in case of RTMFP connection */
        private const DEFAULT_TOR_CLIENT_ADDR:Object = {
            host: "127.0.0.1",
            port: 9002
        };

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Socket to Cirrus server
        private var s_c:CirrusSocket;
        // Socket to facilitator.
        private var s_f:FacilitatorSocket;
        // Handle local-remote traffic
        private var p_p:ProxyPair;

        private var client_id:String;
        private var proxy_pair_factory:Function;

        private var proxy_pairs:Array;

        private var debug_mode:Boolean;
        private var proxy_mode:Boolean;

        /* TextField for debug output. */
        private var output_text:TextField;

        /* Badge for display */
        private var badge:InternetFreedomBadge;

        private var fac_addr:Object;
        private var relay_addr:Object;

        public function swfcat()
        {
            proxy_mode = false;
            debug_mode = false;

            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        public function puts(s:String):void
        {
            if (output_text != null) {
                output_text.appendText(s + "\n");
                output_text.scrollV = output_text.maxScrollV;
            }
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;
            var relay_spec:String;

            debug_mode = (this.loaderInfo.parameters["debug"] != null)
            proxy_mode = (this.loaderInfo.parameters["proxy"] != null);
            if (proxy_mode && !debug_mode) {
                badge = new InternetFreedomBadge(this);
                badge.display();
            } else {
                output_text = new TextField();
                output_text.width = stage.stageWidth;
                output_text.height = stage.stageHeight;
                output_text.background = true;
                output_text.backgroundColor = 0x001f0f;
                output_text.textColor = 0x44cc44;
                addChild(output_text);
            }

            puts("Starting: parameters loaded.");

            /* TODO: use this to have multiple proxies going at once */
            proxy_pairs = new Array();

            fac_spec = this.loaderInfo.parameters["facilitator"];
            if (fac_spec) {
                puts("Facilitator spec: \"" + fac_spec + "\"");
                fac_addr = parse_addr_spec(fac_spec);
                if (!fac_addr) {
                    puts("Error: Facilitator spec must be in the form \"host:port\".");
                    return;
                }
            } else {
                fac_addr = DEFAULT_FACILITATOR_ADDR;
            }

            main();
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function main():void
        {
            if (proxy_mode) {
                establish_facilitator_connection();
            } else {
                establish_cirrus_connection();
            }
        }

        private function establish_cirrus_connection():void
        {
            s_c = new CirrusSocket();
            s_c.addEventListener(CirrusSocketEvent.CONNECT_SUCCESS, function (e:CirrusSocketEvent):void {
                puts("Cirrus: connected with id " + s_c.id + ".");
                if (proxy_mode) {
                    start_proxy_pair();
                    s_c.send_hello(client_id);
                } else {
                    establish_facilitator_connection();
                }
            });
            s_c.addEventListener(CirrusSocketEvent.CONNECT_FAILED, function (e:CirrusSocketEvent):void {
                puts("Error: failed to connect to Cirrus.");
            });
            s_c.addEventListener(CirrusSocketEvent.CONNECT_CLOSED, function (e:CirrusSocketEvent):void {
                puts("Cirrus: closed connection.");
            });
            s_c.addEventListener(CirrusSocketEvent.HELLO_RECEIVED, function (e:CirrusSocketEvent):void {
                puts("Cirrus: received hello from peer " + e.peer);

                /* don't bother if we already have a proxy going */
                if (p_p != null && p_p.connected) {
                    return;
                }

                /* if we're in proxy mode, we should have already set
                   up a proxy pair */
                if (!proxy_mode) {
                    relay_addr = DEFAULT_TOR_CLIENT_ADDR;
                    proxy_pair_factory = rtmfp_proxy_pair_factory;
                    start_proxy_pair();
                    s_c.send_hello(e.peer);
                } else if (!debug_mode && badge != null) {
                    badge.total_proxy_pairs++;
                    badge.num_proxy_pairs++;
                }

                p_p.client = {peer: e.peer, stream: e.stream};
            });

            s_c.connect(DEFAULT_CIRRUS_ADDR, DEFAULT_CIRRUS_KEY);
        }

        private function establish_facilitator_connection():void
        {
            s_f = new FacilitatorSocket(fac_addr.host, fac_addr.port);
            s_f.addEventListener(FacilitatorSocketEvent.CONNECT_FAILED, function (e:Event):void {
                puts("Facilitator: connect failed.");
                setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
            });

            if (proxy_mode) {
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATION_RECEIVED, function (e:FacilitatorSocketEvent):void {
                    var client_addr:Object = parse_addr_spec(e.client);
                    relay_addr = parse_addr_spec(e.relay);
                    if (client_addr == null) {
                        puts("Facilitator: got registration " + e.client);
                        proxy_pair_factory = rtmfp_proxy_pair_factory;
                        if (s_c == null || !s_c.connected) {
                            client_id = e.client;
                            establish_cirrus_connection();
                        } else {
                            start_proxy_pair();
                            s_c.send_hello(e.client);
                        }
                    } else {
                        proxy_pair_factory = tcp_proxy_pair_factory;
                        start_proxy_pair();
                        p_p.client = client_addr;
                    }
                });
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATIONS_EMPTY, function (e:Event):void {
                    puts("Facilitator: no registrations available.");
                    setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
                });
                puts("Facilitator: getting registration.");
                s_f.get_registration();
            } else {
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATION_FAILED, function (e:Event):void {
                    puts("Facilitator: registration failed.");
                    setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
                });
                puts("Facilitator: posting registration.");
                s_f.post_registration(s_c.id);
            }
        }

        private function start_proxy_pair():void
        {
            p_p = proxy_pair_factory();
            p_p.addEventListener(Event.CONNECT, function (e:Event):void {
                puts("ProxyPair: connected!");
            });
            p_p.addEventListener(Event.CLOSE, function (e:Event):void {
                puts("ProxyPair: connection closed.");
                p_p = null;
                if (proxy_mode && !debug_mode && badge != null) {
                    badge.num_proxy_pairs--;
                }
                establish_facilitator_connection();
            });
            p_p.relay = relay_addr;
        }

        private function rtmfp_proxy_pair_factory():ProxyPair
        {
            return new RTMFPProxyPair(this, s_c, s_c.local_stream_name);
        }

        // currently is the same as TCPProxyPair
        // could be interesting to see how this works
        // can't imagine it will work terribly well...
        private function sqs_proxy_pair_factory():ProxyPair
        {
            return new SQSProxyPair(this);
        }

        private function tcp_proxy_pair_factory():ProxyPair
        {
            return new TCPProxyPair(this);
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.text.TextField;
import flash.text.TextFormat;

class InternetFreedomBadge {

    private var ui:swfcat;

    private var _num_proxy_pairs:uint;
    private var _total_proxy_pairs:uint;

    [Embed(source="badge.png")]
    private var BadgeImage:Class;
    private var tot_client_count_tf:TextField;
    private var tot_client_count_fmt:TextFormat;
    private var cur_client_count_tf:TextField;
    private var cur_client_count_fmt:TextFormat;

    public function InternetFreedomBadge(ui:swfcat)
    {
        this.ui = ui;
        _num_proxy_pairs = 0;
        _total_proxy_pairs = 0;

        /* Setup client counter for badge. */
        tot_client_count_fmt = new TextFormat();
        tot_client_count_fmt.color = 0xFFFFFF;
        tot_client_count_fmt.align = "center";
        tot_client_count_fmt.font = "courier-new";
        tot_client_count_fmt.bold = true;
        tot_client_count_fmt.size = 10;
        tot_client_count_tf = new TextField();
        tot_client_count_tf.width = 20;
        tot_client_count_tf.height = 17;
        tot_client_count_tf.background = false;
        tot_client_count_tf.defaultTextFormat = tot_client_count_fmt;
        tot_client_count_tf.x=47;
        tot_client_count_tf.y=0;

        cur_client_count_fmt = new TextFormat();
        cur_client_count_fmt.color = 0xFFFFFF;
        cur_client_count_fmt.align = "center";
        cur_client_count_fmt.font = "courier-new";
        cur_client_count_fmt.bold = true;
        cur_client_count_fmt.size = 10;
        cur_client_count_tf = new TextField();
        cur_client_count_tf.width = 20;
        cur_client_count_tf.height = 17;
        cur_client_count_tf.background = false;
        cur_client_count_tf.defaultTextFormat = cur_client_count_fmt;
        cur_client_count_tf.x=47;
        cur_client_count_tf.y=6;

        /* Update the client counter on badge. */
        update_client_count();
    }

    public function display():void
    {
        ui.addChild(new BadgeImage());
        /* Tried unsuccessfully to add counter to badge. */
        /* For now, need two addChilds :( */
        ui.addChild(tot_client_count_tf);
        ui.addChild(cur_client_count_tf);
    }

    public function get num_proxy_pairs():uint
    {
        return _num_proxy_pairs;
    }

    public function set num_proxy_pairs(amount:uint):void
    {
        _num_proxy_pairs = amount;
        update_client_count();
    }

    public function get total_proxy_pairs():uint
    {
        return _total_proxy_pairs;
    }

    public function set total_proxy_pairs(amount:uint):void
    {
        _total_proxy_pairs = amount;
        /* note: doesn't update, so be sure to update this
           before you update num_proxy_pairs! */
    }

    private function update_client_count():void
    {
        /* Update total client count. */
        if (String(total_proxy_pairs).length == 1)
            tot_client_count_tf.text = "0" + String(total_proxy_pairs);
        else
            tot_client_count_tf.text = String(total_proxy_pairs);

        /* Update current client count. */
        cur_client_count_tf.text = "";
        for(var i:Number = 0; i < num_proxy_pairs; i++)
            cur_client_count_tf.appendText(".");
    }
}

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.events.Event;
    import flash.utils.setTimeout;

    import FacilitatorSocket;
    import events.FacilitatorSocketEvent;

    import ProxyPair;
    import RTMFPProxyPair;
    import SQSProxyPair;
    import TCPProxyPair;

    import rtmfp.CirrusSocket;
    import rtmfp.events.CirrusSocketEvent;

    public class swfcat extends Sprite
    {
        /* Adobe's Cirrus server for RTMFP connections.
           The Cirrus key is defined at compile time by
           reading from the CIRRUS_KEY environment var. */
        private const DEFAULT_CIRRUS_ADDR:String = "rtmfp://p2p.rtmfp.net";
        private const DEFAULT_CIRRUS_KEY:String = RTMFP::CIRRUS_KEY;

        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "tor-facilitator.bamsoftware.com",
            port: 9002
        };

        /* Default Tor client to use in case of RTMFP connection */
        private const DEFAULT_TOR_CLIENT_ADDR:Object = {
            host: "127.0.0.1",
            port: 9002
        };

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Socket to Cirrus server
        private var s_c:CirrusSocket;
        // Socket to facilitator.
        private var s_f:FacilitatorSocket;
        // Handle local-remote traffic
        private var p_p:ProxyPair;

        private var client_id:String;
        private var proxy_pair_factory:Function;

        private var proxy_pairs:Array;

        private var debug_mode:Boolean;
        private var proxy_mode:Boolean;

        /* TextField for debug output. */
        private var output_text:TextField;

        /* Badge for display */
        private var badge:Badge;

        private var fac_addr:Object;
        private var relay_addr:Object;

        public function swfcat()
        {
            proxy_mode = false;
            debug_mode = false;

            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        public function puts(s:String):void
        {
            if (output_text != null) {
                output_text.appendText(s + "\n");
                output_text.scrollV = output_text.maxScrollV;
            }
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;
            var relay_spec:String;

            debug_mode = (this.loaderInfo.parameters["debug"] != null)
            proxy_mode = (this.loaderInfo.parameters["proxy"] != null);
            if (proxy_mode && !debug_mode) {
                badge = new Badge();
                addChild(badge);
            } else {
                output_text = new TextField();
                output_text.width = stage.stageWidth;
                output_text.height = stage.stageHeight;
                output_text.background = true;
                output_text.backgroundColor = 0x001f0f;
                output_text.textColor = 0x44cc44;
                addChild(output_text);
            }

            puts("Starting: parameters loaded.");

            /* TODO: use this to have multiple proxies going at once */
            proxy_pairs = new Array();

            fac_spec = this.loaderInfo.parameters["facilitator"];
            if (fac_spec) {
                puts("Facilitator spec: \"" + fac_spec + "\"");
                fac_addr = parse_addr_spec(fac_spec);
                if (!fac_addr) {
                    puts("Error: Facilitator spec must be in the form \"host:port\".");
                    return;
                }
            } else {
                fac_addr = DEFAULT_FACILITATOR_ADDR;
            }

            main();
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function main():void
        {
            if (proxy_mode) {
                establish_facilitator_connection();
            } else {
                establish_cirrus_connection();
            }
        }

        private function establish_cirrus_connection():void
        {
            s_c = new CirrusSocket();
            s_c.addEventListener(CirrusSocketEvent.CONNECT_SUCCESS, function (e:CirrusSocketEvent):void {
                puts("Cirrus: connected with id " + s_c.id + ".");
                if (proxy_mode) {
                    start_proxy_pair();
                    s_c.send_hello(client_id);
                } else {
                    establish_facilitator_connection();
                }
            });
            s_c.addEventListener(CirrusSocketEvent.CONNECT_FAILED, function (e:CirrusSocketEvent):void {
                puts("Error: failed to connect to Cirrus.");
            });
            s_c.addEventListener(CirrusSocketEvent.CONNECT_CLOSED, function (e:CirrusSocketEvent):void {
                puts("Cirrus: closed connection.");
            });
            s_c.addEventListener(CirrusSocketEvent.HELLO_RECEIVED, function (e:CirrusSocketEvent):void {
                puts("Cirrus: received hello from peer " + e.peer);

                /* don't bother if we already have a proxy going */
                if (p_p != null && p_p.connected) {
                    return;
                }

                /* if we're in proxy mode, we should have already set
                   up a proxy pair */
                if (!proxy_mode) {
                    relay_addr = DEFAULT_TOR_CLIENT_ADDR;
                    proxy_pair_factory = rtmfp_proxy_pair_factory;
                    start_proxy_pair();
                    s_c.send_hello(e.peer);
                } else if (!debug_mode && badge != null) {
                    badge.proxy_begin();
                }

                p_p.client = {peer: e.peer, stream: e.stream};
            });

            s_c.connect(DEFAULT_CIRRUS_ADDR, DEFAULT_CIRRUS_KEY);
        }

        private function establish_facilitator_connection():void
        {
            s_f = new FacilitatorSocket(fac_addr.host, fac_addr.port);
            s_f.addEventListener(FacilitatorSocketEvent.CONNECT_FAILED, function (e:Event):void {
                puts("Facilitator: connect failed.");
                setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
            });

            if (proxy_mode) {
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATION_RECEIVED, function (e:FacilitatorSocketEvent):void {
                    var client_addr:Object = parse_addr_spec(e.client);
                    relay_addr = parse_addr_spec(e.relay);
                    if (client_addr == null) {
                        puts("Facilitator: got registration " + e.client);
                        proxy_pair_factory = rtmfp_proxy_pair_factory;
                        if (s_c == null || !s_c.connected) {
                            client_id = e.client;
                            establish_cirrus_connection();
                        } else {
                            start_proxy_pair();
                            s_c.send_hello(e.client);
                        }
                    } else {
                        proxy_pair_factory = tcp_proxy_pair_factory;
                        start_proxy_pair();
                        p_p.client = client_addr;
                    }
                });
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATIONS_EMPTY, function (e:Event):void {
                    puts("Facilitator: no registrations available.");
                    setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
                });
                puts("Facilitator: getting registration.");
                s_f.get_registration();
            } else {
                s_f.addEventListener(FacilitatorSocketEvent.REGISTRATION_FAILED, function (e:Event):void {
                    puts("Facilitator: registration failed.");
                    setTimeout(establish_facilitator_connection, FACILITATOR_POLL_INTERVAL);
                });
                puts("Facilitator: posting registration.");
                s_f.post_registration(s_c.id);
            }
        }

        private function start_proxy_pair():void
        {
            p_p = proxy_pair_factory();
            p_p.addEventListener(Event.CONNECT, function (e:Event):void {
                puts("ProxyPair: connected!");
            });
            p_p.addEventListener(Event.CLOSE, function (e:Event):void {
                puts("ProxyPair: connection closed.");
                p_p = null;
                if (proxy_mode && !debug_mode && badge != null) {
                    badge.proxy_end();
                }
                establish_facilitator_connection();
            });
            p_p.relay = relay_addr;
        }

        private function rtmfp_proxy_pair_factory():ProxyPair
        {
            return new RTMFPProxyPair(this, s_c, s_c.local_stream_name);
        }

        // currently is the same as TCPProxyPair
        // could be interesting to see how this works
        // can't imagine it will work terribly well...
        private function sqs_proxy_pair_factory():ProxyPair
        {
            return new SQSProxyPair(this);
        }

        private function tcp_proxy_pair_factory():ProxyPair
        {
            return new TCPProxyPair(this);
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.text.TextFormat;
import flash.text.TextField;

class Badge extends flash.display.Sprite
{
    /* Number of proxy pairs currently connected. */
    private var num_proxy_pairs:int = 0;
    /* Number of proxy pairs ever connected. */
    private var total_proxy_pairs:int = 0;

    [Embed(source="badge.png")]
    private var BadgeImage:Class;
    private var tot_client_count_tf:TextField;
    private var tot_client_count_fmt:TextFormat;
    private var cur_client_count_tf:TextField;
    private var cur_client_count_fmt:TextFormat;

    public function Badge()
    {
        /* Setup client counter for badge. */
        tot_client_count_fmt = new TextFormat();
        tot_client_count_fmt.color = 0xFFFFFF;
        tot_client_count_fmt.align = "center";
        tot_client_count_fmt.font = "courier-new";
        tot_client_count_fmt.bold = true;
        tot_client_count_fmt.size = 10;
        tot_client_count_tf = new TextField();
        tot_client_count_tf.width = 20;
        tot_client_count_tf.height = 17;
        tot_client_count_tf.background = false;
        tot_client_count_tf.defaultTextFormat = tot_client_count_fmt;
        tot_client_count_tf.x=47;
        tot_client_count_tf.y=0;

        cur_client_count_fmt = new TextFormat();
        cur_client_count_fmt.color = 0xFFFFFF;
        cur_client_count_fmt.align = "center";
        cur_client_count_fmt.font = "courier-new";
        cur_client_count_fmt.bold = true;
        cur_client_count_fmt.size = 10;
        cur_client_count_tf = new TextField();
        cur_client_count_tf.width = 20;
        cur_client_count_tf.height = 17;
        cur_client_count_tf.background = false;
        cur_client_count_tf.defaultTextFormat = cur_client_count_fmt;
        cur_client_count_tf.x=47;
        cur_client_count_tf.y=6;

        addChild(new BadgeImage());
        addChild(tot_client_count_tf);
        addChild(cur_client_count_tf);

        /* Update the client counter on badge. */
        update_client_count();
    }

    public function proxy_begin():void
    {
        num_proxy_pairs++;
        total_proxy_pairs++;
        update_client_count();
    }

    public function proxy_end():void
    {
        num_proxy_pairs--;
        update_client_count();
    }

    private function update_client_count():void
    {
        /* Update total client count. */
        if (String(total_proxy_pairs).length == 1)
            tot_client_count_tf.text = "0" + String(total_proxy_pairs);
        else
            tot_client_count_tf.text = String(total_proxy_pairs);

        /* Update current client count. */
        cur_client_count_tf.text = "";
        for(var i:Number = 0; i < num_proxy_pairs; i++)
            cur_client_count_tf.appendText(".");
    }
}

Refactor InternetFreedomBadge (now Badge).

Make it its own Sprite, don't cause it to change the UI of the main
display. Use proxy_begin and proxy_end methods instead of modifying
current/total counts directly.


//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.desktop.NativeApplication;
import flash.display.NativeMenu;
import flash.display.NativeMenuItem;
import flash.display.NativeWindow;
import flash.display.Stage;
import flash.display.StageQuality;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filesystem.File;
import flash.net.SharedObject;
import flash.utils.IDataOutput;

import flump.executor.Executor;
import flump.executor.Future;
import flump.export.Ternary;
import flump.xfl.ParseError;
import flump.xfl.XflLibrary;

import mx.collections.ArrayList;
import mx.events.CollectionEvent;
import mx.events.PropertyChangeEvent;

import spark.components.DataGrid;
import spark.components.DropDownList;
import spark.components.List;
import spark.components.Window;
import spark.events.GridSelectionEvent;

import com.threerings.util.F;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;

import starling.display.Sprite;

public class Exporter
{
    public static const NA :NativeApplication = NativeApplication.nativeApplication;

    public function Exporter (win :ExporterWindow) {
        Log.setLevel("", Log.INFO);
        _win = win;
        _errors = _win.errors;
        _libraries = _win.libraries;

        function updatePreviewAndExport (..._) :void {
            _win.export.enabled = _exportChooser.dir != null && _libraries.selectionLength > 0 &&
                _libraries.selectedItems.some(function (status :DocStatus, ..._) :Boolean {
                    return status.isValid;
            });

            var status :DocStatus = _libraries.selectedItem as DocStatus;
            _win.preview.enabled = status != null && status.isValid;

            if (_exportChooser.dir == null) return;
            _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
        }

        var fileMenuItem :NativeMenuItem;
        if (NativeApplication.supportsMenu) {
            // Grab the existing menu on macs. Use an index to get it as it's not going to be
            // 'File' in all languages
            fileMenuItem = NA.menu.getItemAt(1);
            // Add a separator before the existing close command
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 0);
        } else {
            _win.nativeWindow.menu = new NativeMenu();
            fileMenuItem = _win.nativeWindow.menu.addSubmenu(new NativeMenu(), "File");
        }

        // Add save and save as by index to work with the existing items on Mac
        // Mac menus have an existing "Close" item, so everything we add should go ahead of that
        var newMenuItem :NativeMenuItem = fileMenuItem.submenu.addItemAt(new NativeMenuItem("New"), 0);
        newMenuItem.keyEquivalent = "n";
        newMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            _confFile = null;
            _conf = new FlumpConf();
            updatePublisher();
        });
        var openMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Open..."), 1);
        openMenuItem.keyEquivalent = "o";
        openMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                openConf();
                updatePublisher();
            });
            file.browseForOpen("Open Flump Configuration");
        });
        fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 2);

        const saveMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save"), 3);
        saveMenuItem.keyEquivalent = "s";
        function saveConf () :void {
            Files.write(_confFile, function (out :IDataOutput) :void {
                // Set directories relative to where this file is being saved. Fall back to absolute
                // paths if relative paths aren't possible.
                if (_importChooser.dir != null) {
                    _conf.importDir = _confFile.parent.getRelativePath(_importChooser.dir, /*useDotDot=*/true);
                    if (_conf.importDir == null) _conf.importDir = _importChooser.dir.nativePath;
                }

                if (_exportChooser.dir != null) {
                    _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
                    if (_conf.exportDir == null) _conf.exportDir = _exportChooser.dir.nativePath;
                }

                out.writeUTFBytes(JSON.stringify(_conf, null, /*space=*/2));
            });
        };
        function saveAs (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                trace("Conf file is now " + _confFile.nativePath);
                _settings.data["CONF_FILE"] = _confFile.nativePath;
                _settings.flush();
                saveConf();
            });
            file.browseForSave("Save Flump Configuration");
        };
        saveMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            if (_confFile == null) saveAs();
            else saveConf();
        });

        function openConf () :void {
            try {
                _conf = FlumpConf.fromJSON(JSONFormat.readJSON(_confFile));
                _win.title = _confFile.name;
                var dir :String = _confFile.parent.resolvePath(_conf.importDir).nativePath;
                setImport(new File(dir));
            } catch (e :Error) {
                log.warning("Unable to parse conf", e);
                _errors.dataProvider.addItem(new ParseError(_confFile.nativePath,
                    ParseError.CRIT, "Unable to read configuration"));
                _confFile = null;
            }
        };

        const saveAsMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save As..."), 4);
        saveAsMenuItem.keyEquivalent = "S";
        saveAsMenuItem.addEventListener(Event.SELECT, saveAs);

        if (_settings.data.hasOwnProperty("CONF_FILE")) {
            _confFile = new File(_settings.data["CONF_FILE"]);
            openConf();
        }

        var curSelection :DocStatus = null;
        _libraries.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
            log.info("Changed", "selected", _libraries.selectedIndices);
            updatePreviewAndExport();

            if (curSelection != null) {
                curSelection.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            var newSelection :DocStatus = _libraries.selectedItem as DocStatus;
            if (newSelection != null) {
                newSelection.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            curSelection = newSelection;
        });
        _win.reload.addEventListener(MouseEvent.CLICK, function (..._) :void {
            setImport(_importChooser.dir);
            updatePreviewAndExport();
        });
        _win.export.addEventListener(MouseEvent.CLICK, function (..._) :void {
            for each (var status :DocStatus in _libraries.selectedItems) {
                exportFlashDocument(status);
            }
        });
        _win.preview.addEventListener(MouseEvent.CLICK, function (..._) :void {
            showPreviewWindow(_libraries.selectedItem.lib);
        });
        _importChooser = new DirChooser(null, _win.importRoot, _win.browseImport);
        _importChooser.changed.add(setImport);
        _exportChooser = new DirChooser(null, _win.exportRoot, _win.browseExport);
        _exportChooser.changed.add(updatePreviewAndExport);
        function updatePublisher (..._) :void {
            if (_confFile != null) {
                _importChooser.dir = (_conf.importDir != null) ? _confFile.parent.resolvePath(_conf.importDir) : null;
                _exportChooser.dir = (_conf.exportDir != null) ? _confFile.parent.resolvePath(_conf.exportDir) : null;
            } else {
                _importChooser.dir = null;
                _exportChooser.dir = null;
            }
            if (_exportChooser.dir == null || _conf.exports.length == 0) _publisher = null;
            else _publisher = new Publisher(_exportChooser.dir, Vector.<ExportConf>(_conf.exports));

            var formatNames :Array = [];
            for each (var export :ExportConf in _conf.exports) formatNames.push(export.name);
            _win.formatOverview.text = formatNames.join(", ");
        };
        _exportChooser.changed.add(updatePublisher);

        var editFormats :EditFormatsWindow;
        _win.editFormats.addEventListener(MouseEvent.CLICK, function (..._) :void {
            if (editFormats == null || editFormats.closed) {
                editFormats = new EditFormatsWindow();
                editFormats.open();
            } else editFormats.orderToFront();

            var dataProvider :ArrayList = new ArrayList(_conf.exports);
            dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE, updatePublisher);

            editFormats.exports.dataProvider = dataProvider;
            editFormats.buttonAdd.addEventListener(MouseEvent.CLICK, function (..._) :void {
                var export :ExportConf = new ExportConf();
                export.name = "format" + (_conf.exports.length+1);
                if (_conf.exports.length > 0) {
                    export.format = _conf.exports[0].format;
                }
                dataProvider.addItem(export);
            });
            editFormats.exports.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
                editFormats.buttonRemove.enabled = (editFormats.exports.selectedItem != null);
            });
            editFormats.buttonRemove.addEventListener(MouseEvent.CLICK, function (..._) :void {
                for each (var export :ExportConf in editFormats.exports.selectedItems) {
                    dataProvider.removeItem(export);
                }
            });
        });

        updatePublisher();
        _win.addEventListener(Event.CLOSE, function (..._) :void { NA.exit(0); });
    }

    protected function setImport (root :File) :void {
        _libraries.dataProvider.removeAll();
        _errors.dataProvider.removeAll();
        if (root == null) return;
        _rootLen = root.nativePath.length + 1;
        if (_docFinder != null) _docFinder.shutdownNow();
        _docFinder = new Executor();
        findFlashDocuments(root, _docFinder, true);
        _win.reload.enabled = true;
    }

    protected function showPreviewWindow (lib :XflLibrary) :void {
        if (_previewController == null || _previewWindow.closed || _previewControls.closed) {
            _previewWindow = new PreviewWindow();
            _previewControls = new PreviewControlsWindow();
            _previewWindow.started = function (container :Sprite) :void {
                _previewController = new PreviewController(lib, container, _previewWindow,
                    _previewControls);
            }

            _previewWindow.open();
            _previewControls.open();

            preventWindowClose(_previewWindow.nativeWindow);
            preventWindowClose(_previewControls.nativeWindow);

        } else {
            _previewController.lib = lib;
            _previewWindow.nativeWindow.visible = true;
            _previewControls.nativeWindow.visible = true;
        }

        _previewWindow.orderToFront();
        _previewControls.orderToFront();
    }

    // Causes a window to be hidden, rather than closed, when its close box is clicked
    protected static function preventWindowClose (window :NativeWindow) :void {
        window.addEventListener(Event.CLOSING, function (e :Event) :void {
            e.preventDefault();
            window.visible = false;
        });
    }

    protected var _previewController :PreviewController;
    protected var _previewWindow :PreviewWindow;
    protected var _previewControls :PreviewControlsWindow;

    protected function findFlashDocuments (base :File, exec :Executor,
        ignoreXflAtBase :Boolean = false) :void {
        Files.list(base, exec).succeeded.add(function (files :Array) :void {
            if (exec.isShutdown) return;
            for each (var file :File in files) {
                if (Files.hasExtension(file, "xfl")) {
                    if (ignoreXflAtBase) {
                        _errors.dataProvider.addItem(new ParseError(base.nativePath,
                            ParseError.CRIT, "The import directory can't be an XFL directory, did you mean " +
                            base.parent.nativePath + "?"));
                    } else addFlashDocument(file);
                    return;
                }
            }
            for each (file in files) {
                if (StringUtil.startsWith(file.name, ".", "RECOVER_")) {
                    continue; // Ignore hidden VCS directories, and recovered backups created by Flash
                }
                if (file.isDirectory) findFlashDocuments(file, exec);
                else addFlashDocument(file);
            }
        });
    }

    protected function exportFlashDocument (status :DocStatus) :void {
        const stage :Stage = NA.activeWindow.stage;
        const prevQuality :String = stage.quality;

        stage.quality = StageQuality.BEST;
        _publisher.publish(status.lib);

        stage.quality = prevQuality;
        status.updateModified(Ternary.FALSE);
    }

    protected function addFlashDocument (file :File) :void {
        var name :String = file.nativePath.substring(_rootLen).replace(
            new RegExp("\\" + File.separator, "g"), "/");
        var load :Future;
        switch (Files.getExtension(file)) {
        case "xfl":
            name = name.substr(0, name.lastIndexOf("/"));
            load = new XflLoader().load(name, file.parent);
            break;
        case "fla":
            name = name.substr(0, name.lastIndexOf("."));
            load = new FlaLoader().load(name, file);
            break;
        default:
            // Unsupported file type, ignore
            return;
        }

        const status :DocStatus = new DocStatus(name, _rootLen, Ternary.UNKNOWN, Ternary.UNKNOWN, null);
        _libraries.dataProvider.addItem(status);

        load.succeeded.add(function (lib :XflLibrary) :void {
            status.lib = lib;
            status.updateModified(Ternary.of(_publisher == null || _publisher.modified(lib)));
            for each (var err :ParseError in lib.getErrors()) _errors.dataProvider.addItem(err);
            status.updateValid(Ternary.of(lib.valid));
        });
        load.failed.add(function (error :Error) :void {
            trace("Failed to load " + file.nativePath + ": " + error);
            status.updateValid(Ternary.FALSE);
            throw error;
        });
    }

    protected var _rootLen :int;

    protected var _publisher :Publisher;
    protected var _docFinder :Executor;
    protected var _win :ExporterWindow;
    protected var _libraries :DataGrid;
    protected var _errors :DataGrid;
    protected var _exportChooser :DirChooser;
    protected var _importChooser :DirChooser;
    protected var _authoredResolution :DropDownList;
    protected var _conf :FlumpConf = new FlumpConf();
    protected var _confFile :File;
    protected const _settings :SharedObject = SharedObject.getLocal("flump/Exporter");

    private static const log :Log = Log.getLog(Exporter);
}
}

import flash.events.EventDispatcher;

import flump.export.Ternary;
import flump.xfl.XflLibrary;

import mx.core.IPropertyChangeNotifier;
import mx.events.PropertyChangeEvent;

class DocStatus extends EventDispatcher implements IPropertyChangeNotifier {
    public var path :String;
    public var modified :String;
    public var valid :String = QUESTION;
    public var lib :XflLibrary;

    public function DocStatus (path :String, rootLen :int, modified :Ternary, valid :Ternary, lib :XflLibrary) {
        this.lib = lib;
        this.path = path;
        _uid = path;

        updateModified(modified);
        updateValid(valid);
    }

    public function updateValid (newValid :Ternary) :void {
        changeField("valid", function (..._) :void {
            if (newValid == Ternary.TRUE) valid = CHECK;
            else if (newValid == Ternary.FALSE) valid = FROWN;
            else valid = QUESTION;
        });
    }

    public function get isValid () :Boolean { return valid == CHECK; }

    public function updateModified (newModified :Ternary) :void {
        changeField("modified", function (..._) :void {
            if (newModified == Ternary.TRUE) modified = CHECK;
            else if (newModified == Ternary.FALSE) modified = " ";
            else modified = QUESTION;
        });
    }

    protected function changeField(fieldName :String, modifier :Function) :void {
        const oldValue :Object = this[fieldName];
        modifier();
        const newValue :Object = this[fieldName];
        dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, fieldName, oldValue, newValue));
    }

    public function get uid () :String { return _uid; }
    public function set uid (uid :String) :void { _uid = uid; }

    protected var _uid :String;

    protected static const QUESTION :String = "?";
    protected static const FROWN :String = "☹";
    protected static const CHECK :String = "✓";
}

//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.desktop.NativeApplication;
import flash.display.NativeMenu;
import flash.display.NativeMenuItem;
import flash.display.NativeWindow;
import flash.display.Stage;
import flash.display.StageQuality;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filesystem.File;
import flash.net.SharedObject;
import flash.utils.IDataOutput;

import flump.executor.Executor;
import flump.executor.Future;
import flump.export.Ternary;
import flump.xfl.ParseError;
import flump.xfl.XflLibrary;

import mx.collections.ArrayList;
import mx.events.CollectionEvent;
import mx.events.PropertyChangeEvent;

import spark.components.DataGrid;
import spark.components.DropDownList;
import spark.components.List;
import spark.components.Window;
import spark.events.GridSelectionEvent;

import com.threerings.util.F;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;

import starling.display.Sprite;

public class Exporter
{
    public static const NA :NativeApplication = NativeApplication.nativeApplication;

    public function Exporter (win :ExporterWindow) {
        Log.setLevel("", Log.INFO);
        _win = win;
        _errors = _win.errors;
        _libraries = _win.libraries;

        function updatePreviewAndExport (..._) :void {
            _win.export.enabled = _exportChooser.dir != null && _libraries.selectionLength > 0 &&
                _libraries.selectedItems.some(function (status :DocStatus, ..._) :Boolean {
                    return status.isValid;
            });

            var status :DocStatus = _libraries.selectedItem as DocStatus;
            _win.preview.enabled = status != null && status.isValid;

            if (_exportChooser.dir == null) return;
            _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
        }

        var fileMenuItem :NativeMenuItem;
        if (NativeApplication.supportsMenu) {
            // Grab the existing menu on macs. Use an index to get it as it's not going to be
            // 'File' in all languages
            fileMenuItem = NA.menu.getItemAt(1);
            // Add a separator before the existing close command
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 0);
        } else {
            _win.nativeWindow.menu = new NativeMenu();
            fileMenuItem = _win.nativeWindow.menu.addSubmenu(new NativeMenu(), "File");
        }

        // Add save and save as by index to work with the existing items on Mac
        // Mac menus have an existing "Close" item, so everything we add should go ahead of that
        var newMenuItem :NativeMenuItem = fileMenuItem.submenu.addItemAt(new NativeMenuItem("New"), 0);
        newMenuItem.keyEquivalent = "n";
        newMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            _confFile = null;
            _conf = new FlumpConf();
            updatePublisher();
        });
        var openMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Open..."), 1);
        openMenuItem.keyEquivalent = "o";
        openMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                openConf();
                updatePublisher();
            });
            file.browseForOpen("Open Flump Configuration");
        });
        fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 2);

        const saveMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save"), 3);
        saveMenuItem.keyEquivalent = "s";
        function saveConf () :void {
            Files.write(_confFile, function (out :IDataOutput) :void {
                // Set directories relative to where this file is being saved. Fall back to absolute
                // paths if relative paths aren't possible.
                if (_importChooser.dir != null) {
                    _conf.importDir = _confFile.parent.getRelativePath(_importChooser.dir, /*useDotDot=*/true);
                    if (_conf.importDir == null) _conf.importDir = _importChooser.dir.nativePath;
                }

                if (_exportChooser.dir != null) {
                    _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
                    if (_conf.exportDir == null) _conf.exportDir = _exportChooser.dir.nativePath;
                }

                out.writeUTFBytes(JSON.stringify(_conf, null, /*space=*/2));
            });
        };
        function saveAs (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                trace("Conf file is now " + _confFile.nativePath);
                _settings.data["CONF_FILE"] = _confFile.nativePath;
                _settings.flush();
                saveConf();
            });
            file.browseForSave("Save Flump Configuration");
        };
        saveMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            if (_confFile == null) saveAs();
            else saveConf();
        });

        function openConf () :void {
            try {
                _conf = FlumpConf.fromJSON(JSONFormat.readJSON(_confFile));
                _win.title = _confFile.name;
                var dir :String = _confFile.parent.resolvePath(_conf.importDir).nativePath;
                setImport(new File(dir));
            } catch (e :Error) {
                log.warning("Unable to parse conf", e);
                _errors.dataProvider.addItem(new ParseError(_confFile.nativePath,
                    ParseError.CRIT, "Unable to read configuration"));
                _confFile = null;
            }
        };

        const saveAsMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save As..."), 4);
        saveAsMenuItem.keyEquivalent = "S";
        saveAsMenuItem.addEventListener(Event.SELECT, saveAs);

        if (_settings.data.hasOwnProperty("CONF_FILE")) {
            _confFile = new File(_settings.data["CONF_FILE"]);
            openConf();
        }

        var curSelection :DocStatus = null;
        _libraries.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
            log.info("Changed", "selected", _libraries.selectedIndices);
            updatePreviewAndExport();

            if (curSelection != null) {
                curSelection.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            var newSelection :DocStatus = _libraries.selectedItem as DocStatus;
            if (newSelection != null) {
                newSelection.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            curSelection = newSelection;
        });
        _win.reload.addEventListener(MouseEvent.CLICK, function (..._) :void {
            setImport(_importChooser.dir);
            updatePreviewAndExport();
        });
        _win.export.addEventListener(MouseEvent.CLICK, function (..._) :void {
            for each (var status :DocStatus in _libraries.selectedItems) {
                exportFlashDocument(status);
            }
        });
        _win.preview.addEventListener(MouseEvent.CLICK, function (..._) :void {
            showPreviewWindow(_libraries.selectedItem.lib);
        });
        _importChooser = new DirChooser(null, _win.importRoot, _win.browseImport);
        _importChooser.changed.add(setImport);
        _exportChooser = new DirChooser(null, _win.exportRoot, _win.browseExport);
        _exportChooser.changed.add(updatePreviewAndExport);

        function updatePublisher (..._) :void {
            if (_confFile != null) {
                _importChooser.dir = (_conf.importDir != null) ? _confFile.parent.resolvePath(_conf.importDir) : null;
                _exportChooser.dir = (_conf.exportDir != null) ? _confFile.parent.resolvePath(_conf.exportDir) : null;
            } else {
                _importChooser.dir = null;
                _exportChooser.dir = null;
            }
            if (_exportChooser.dir == null || _conf.exports.length == 0) _publisher = null;
            else _publisher = new Publisher(_exportChooser.dir, Vector.<ExportConf>(_conf.exports));

            var formatNames :Array = [];
            for each (var export :ExportConf in _conf.exports) formatNames.push(export.name);
            _win.formatOverview.text = formatNames.join(", ");
        };

        var editFormats :EditFormatsWindow;
        _win.editFormats.addEventListener(MouseEvent.CLICK, function (..._) :void {
            if (editFormats == null || editFormats.closed) {
                editFormats = new EditFormatsWindow();
                editFormats.open();
            } else editFormats.orderToFront();

            var dataProvider :ArrayList = new ArrayList(_conf.exports);
            dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE, updatePublisher);

            editFormats.exports.dataProvider = dataProvider;
            editFormats.buttonAdd.addEventListener(MouseEvent.CLICK, function (..._) :void {
                var export :ExportConf = new ExportConf();
                export.name = "format" + (_conf.exports.length+1);
                if (_conf.exports.length > 0) {
                    export.format = _conf.exports[0].format;
                }
                dataProvider.addItem(export);
            });
            editFormats.exports.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
                editFormats.buttonRemove.enabled = (editFormats.exports.selectedItem != null);
            });
            editFormats.buttonRemove.addEventListener(MouseEvent.CLICK, function (..._) :void {
                for each (var export :ExportConf in editFormats.exports.selectedItems) {
                    dataProvider.removeItem(export);
                }
            });
        });

        updatePublisher();
        _win.addEventListener(Event.CLOSE, function (..._) :void { NA.exit(0); });
    }

    protected function setImport (root :File) :void {
        _libraries.dataProvider.removeAll();
        _errors.dataProvider.removeAll();
        if (root == null) return;
        _rootLen = root.nativePath.length + 1;
        if (_docFinder != null) _docFinder.shutdownNow();
        _docFinder = new Executor();
        findFlashDocuments(root, _docFinder, true);
        _win.reload.enabled = true;
    }

    protected function showPreviewWindow (lib :XflLibrary) :void {
        if (_previewController == null || _previewWindow.closed || _previewControls.closed) {
            _previewWindow = new PreviewWindow();
            _previewControls = new PreviewControlsWindow();
            _previewWindow.started = function (container :Sprite) :void {
                _previewController = new PreviewController(lib, container, _previewWindow,
                    _previewControls);
            }

            _previewWindow.open();
            _previewControls.open();

            preventWindowClose(_previewWindow.nativeWindow);
            preventWindowClose(_previewControls.nativeWindow);

        } else {
            _previewController.lib = lib;
            _previewWindow.nativeWindow.visible = true;
            _previewControls.nativeWindow.visible = true;
        }

        _previewWindow.orderToFront();
        _previewControls.orderToFront();
    }

    // Causes a window to be hidden, rather than closed, when its close box is clicked
    protected static function preventWindowClose (window :NativeWindow) :void {
        window.addEventListener(Event.CLOSING, function (e :Event) :void {
            e.preventDefault();
            window.visible = false;
        });
    }

    protected var _previewController :PreviewController;
    protected var _previewWindow :PreviewWindow;
    protected var _previewControls :PreviewControlsWindow;

    protected function findFlashDocuments (base :File, exec :Executor,
        ignoreXflAtBase :Boolean = false) :void {
        Files.list(base, exec).succeeded.add(function (files :Array) :void {
            if (exec.isShutdown) return;
            for each (var file :File in files) {
                if (Files.hasExtension(file, "xfl")) {
                    if (ignoreXflAtBase) {
                        _errors.dataProvider.addItem(new ParseError(base.nativePath,
                            ParseError.CRIT, "The import directory can't be an XFL directory, did you mean " +
                            base.parent.nativePath + "?"));
                    } else addFlashDocument(file);
                    return;
                }
            }
            for each (file in files) {
                if (StringUtil.startsWith(file.name, ".", "RECOVER_")) {
                    continue; // Ignore hidden VCS directories, and recovered backups created by Flash
                }
                if (file.isDirectory) findFlashDocuments(file, exec);
                else addFlashDocument(file);
            }
        });
    }

    protected function exportFlashDocument (status :DocStatus) :void {
        const stage :Stage = NA.activeWindow.stage;
        const prevQuality :String = stage.quality;

        stage.quality = StageQuality.BEST;
        _publisher.publish(status.lib);

        stage.quality = prevQuality;
        status.updateModified(Ternary.FALSE);
    }

    protected function addFlashDocument (file :File) :void {
        var name :String = file.nativePath.substring(_rootLen).replace(
            new RegExp("\\" + File.separator, "g"), "/");
        var load :Future;
        switch (Files.getExtension(file)) {
        case "xfl":
            name = name.substr(0, name.lastIndexOf("/"));
            load = new XflLoader().load(name, file.parent);
            break;
        case "fla":
            name = name.substr(0, name.lastIndexOf("."));
            load = new FlaLoader().load(name, file);
            break;
        default:
            // Unsupported file type, ignore
            return;
        }

        const status :DocStatus = new DocStatus(name, _rootLen, Ternary.UNKNOWN, Ternary.UNKNOWN, null);
        _libraries.dataProvider.addItem(status);

        load.succeeded.add(function (lib :XflLibrary) :void {
            status.lib = lib;
            status.updateModified(Ternary.of(_publisher == null || _publisher.modified(lib)));
            for each (var err :ParseError in lib.getErrors()) _errors.dataProvider.addItem(err);
            status.updateValid(Ternary.of(lib.valid));
        });
        load.failed.add(function (error :Error) :void {
            trace("Failed to load " + file.nativePath + ": " + error);
            status.updateValid(Ternary.FALSE);
            throw error;
        });
    }

    protected var _rootLen :int;

    protected var _publisher :Publisher;
    protected var _docFinder :Executor;
    protected var _win :ExporterWindow;
    protected var _libraries :DataGrid;
    protected var _errors :DataGrid;
    protected var _exportChooser :DirChooser;
    protected var _importChooser :DirChooser;
    protected var _authoredResolution :DropDownList;
    protected var _conf :FlumpConf = new FlumpConf();
    protected var _confFile :File;
    protected const _settings :SharedObject = SharedObject.getLocal("flump/Exporter");

    private static const log :Log = Log.getLog(Exporter);
}
}

import flash.events.EventDispatcher;

import flump.export.Ternary;
import flump.xfl.XflLibrary;

import mx.core.IPropertyChangeNotifier;
import mx.events.PropertyChangeEvent;

class DocStatus extends EventDispatcher implements IPropertyChangeNotifier {
    public var path :String;
    public var modified :String;
    public var valid :String = QUESTION;
    public var lib :XflLibrary;

    public function DocStatus (path :String, rootLen :int, modified :Ternary, valid :Ternary, lib :XflLibrary) {
        this.lib = lib;
        this.path = path;
        _uid = path;

        updateModified(modified);
        updateValid(valid);
    }

    public function updateValid (newValid :Ternary) :void {
        changeField("valid", function (..._) :void {
            if (newValid == Ternary.TRUE) valid = CHECK;
            else if (newValid == Ternary.FALSE) valid = FROWN;
            else valid = QUESTION;
        });
    }

    public function get isValid () :Boolean { return valid == CHECK; }

    public function updateModified (newModified :Ternary) :void {
        changeField("modified", function (..._) :void {
            if (newModified == Ternary.TRUE) modified = CHECK;
            else if (newModified == Ternary.FALSE) modified = " ";
            else modified = QUESTION;
        });
    }

    protected function changeField(fieldName :String, modifier :Function) :void {
        const oldValue :Object = this[fieldName];
        modifier();
        const newValue :Object = this[fieldName];
        dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, fieldName, oldValue, newValue));
    }

    public function get uid () :String { return _uid; }
    public function set uid (uid :String) :void { _uid = uid; }

    protected var _uid :String;

    protected static const QUESTION :String = "?";
    protected static const FROWN :String = "☹";
    protected static const CHECK :String = "✓";
}

Fix for export directory not updating.

Fix for export directory not updating.