Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add photo uploads

  • Loading branch information...
commit e7655ebb18300efcb54e35da8967e9b67ecc50c6 1 parent 19e20d7
keito authored
View
2  fb/FBAPI.as
@@ -107,7 +107,7 @@ package fb {
request.method = URLRequestMethod.POST;
request.contentType = "multipart/form-data; boundary="+FBPost.boundary;
request.data = post.data;
- loader.dataFormat = URLLoaderDataFormat.BINARY;
+ loader.dataFormat = URLLoaderDataFormat.TEXT;
loader.load(request);
return loader;
View
18 fb/util/StringUtil.as
@@ -122,5 +122,23 @@ package fb.util {
return output;
}
+
+ // Gets the low-order bits of a 64-bit decimal number represented in the
+ // input string. Because ActionScript Numbers are only accurate up to 53
+ // bits when calculating, avoids dealing with any Numbers with more than
+ // 32 bits of significance.
+ public static function lowOrder64(input:String):int {
+ var j:Number = 1;
+ var result:int = 0;
+ for (var i:int = input.length - 1; i >= 0; i--) {
+ var digitValue:Number = int(input.charAt(i)) * j;
+ var digitBin:String = digitValue.toString(2);
+ if (digitBin.length > 32)
+ digitBin = digitBin.substr(digitBin.length - 32, 32);
+ result += parseInt(digitBin, 2);
+ j *= 10;
+ }
+ return result;
+ }
}
}
View
341 fbair/composer/Composer.mxml
@@ -15,47 +15,131 @@
-->
<!-- There's one of these at the top-level.
Major container for letting the user share stuff! -->
-<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
- xmlns:util="fbair.util.display.*" >
-
+<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"
+ xmlns:util="fbair.util.display.*"
+ initialize="init()"
+ verticalGap="5">
<mx:Metadata>
[Event(name="statusUpdated", type="fb.FBEvent")]
</mx:Metadata>
- <!-- Text area for entering your status update -->
- <util:GrowableTextArea id="composerInput"
- styleName="composerInput"
- width="100%"
- focusOutText="What's on your mind?"
- focusInHeight="{
- StylePrefs.sizeStyle == StylePrefs.SIZE_LARGE ?
- 48 : 40}"
- focusOutHeight="{
- StylePrefs.sizeStyle == StylePrefs.SIZE_LARGE ?
- 29 : 25}" />
-
- <util:FBButton id="shareButton"
- autoStyle="true"
- click="shareStatus(event)" >
- <mx:Label styleName="fbButtonLabel"
- text="Share" />
- </util:FBButton>
+ <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
+ xmlns:util="fbair.util.display.*"
+ width="100%">
+
+ <!-- Text area for entering your status update -->
+ <util:GrowableTextArea id="composerInput"
+ styleName="composerInput"
+ width="100%"
+ focusOutText="{FOCUS_OUT_STATUS}"
+ focusInHeight="{
+ StylePrefs.sizeStyle == StylePrefs.SIZE_LARGE ?
+ 48 : 40}"
+ focusOutHeight="{
+ StylePrefs.sizeStyle == StylePrefs.SIZE_LARGE ?
+ 29 : 25}"
+ nativeDragEnter="dragEnter(event)"
+ nativeDragDrop="dragDrop(event)" />
+
+ <util:FBButton id="shareButton"
+ autoStyle="true"
+ click="shareStatus(event)" >
+ <mx:Label styleName="fbButtonLabel"
+ text="Share" />
+ </util:FBButton>
+ </mx:HBox>
+
+ <mx:HBox id="albumName"
+ horizontalGap="5"
+ width="100%"
+ visible="false"
+ includeInLayout="false">
+ <mx:Label text="Album name:"
+ styleName="albumFieldLabel" />
+ <mx:TextInput id="albumNameInput"
+ width="100%"
+ styleName="albumFieldText" />
+ </mx:HBox>
+
+ <mx:TileList id="droppedPhotoList"
+ width="100%"
+ rowCount="1"
+ columnCount="{3 > droppedPhotos.length ? droppedPhotos.length : 3}"
+ dataProvider="{droppedPhotos}"
+ includeInLayout="false"
+ visible="false"
+ styleName="droppedPhotoList"
+ updateComplete="repaintPhotoList()" />
+ <!-- Adding the resize effect appears to add a weird unwanted width resizing,
+ and the original 100% width designation ceases to have any effect during
+ resizing. 12 is the exact pixel value of the composer's desired width-->
+ <mx:Resize id="photosAppear"
+ widthFrom="{width - 12}"
+ widthTo="{width - 12}"
+ heightFrom="0"
+ heightTo="200"
+ target="{droppedPhotoList}"
+ effectEnd="addQueuedPhotosToGrid()" />
+ <mx:Resize id="photosDisappear"
+ heightFrom="200"
+ heightTo="0"
+ target="{droppedPhotoList}"
+ effectEnd="photosFinishedDisappearing()" />
<mx:Script><![CDATA[
+ import mx.utils.ObjectProxy;
+ import mx.collections.ArrayCollection;
import fb.FBAPI;
import fb.FBConnect;
import fb.FBEvent;
import fb.util.Output;
+ import fb.util.StringUtil;
import fbair.gui.StylePrefs;
import fbair.nile.NileContainer;
+ import fbair.composer.DroppedPhotosRenderer;
+
+ import flash.desktop.ClipboardFormats;
+ import flash.desktop.NativeDragManager;
import flash.events.MouseEvent;
+ import flash.events.NativeDragEvent;
import mx.core.Application;
+ import flash.net.navigateToURL;
+ import flash.net.URLRequest;
+
private var submittedMessage:String;
+ private var droppedPhotosRendererFactory:ClassFactory;
+
+ [Bindable]
+ private var droppedPhotos:ArrayCollection = new ArrayCollection();
+
+ private var dropQueuedPhotos:Array = new Array();
+
+ private var remainingUploads:int = 0;
+
+ private static const FOCUS_OUT_STATUS:String = "What's on your mind?";
+ private static const FOCUS_OUT_CAPTION:String = "Describe your photo";
+ private static const FOCUS_OUT_CAPTIONS:String = "Describe your photos";
+
+ private static const APP_DEFAULT_ALBUM_NAME:String =
+ "Facebook for Adobe AIR Photos";
+
+ private static const URL_EDIT_ALBUM:String =
+ "http://www.facebook.com/editphoto.php?aid=";
+
+ // Called when the composer is initialized
+ private function init():void {
+ droppedPhotosRendererFactory = new ClassFactory(DroppedPhotosRenderer);
+ droppedPhotosRendererFactory.properties = {
+ deleteFunction: deletePhoto
+ };
+ droppedPhotoList.itemRenderer = droppedPhotosRendererFactory;
+ }
+
// Called when the Share button has been clicked
private function shareStatus(event:MouseEvent):void {
if (event.shiftKey) {
@@ -64,12 +148,16 @@
}
if (!composerInput.active) {
- Application.application.focusManager.setFocus(composerInput);
- return;
+ if (droppedPhotos.length == 0)
+ composerInput.text = '';
+ else {
+ Application.application.focusManager.setFocus(composerInput);
+ return;
+ }
}
- // Don't share a blank string!
- if (composerInput.text.length == 0) return;
+ // Don't share a blank string! But only if it's a status update.
+ if (composerInput.text.length == 0 && droppedPhotos.length == 0) return;
// Disenable (more popularly known as disable)
submittedMessage = composerInput.text;
@@ -97,11 +185,66 @@
}
}
else {
- FBAPI.callMethod("stream.publish", publish_options)
- .addEventListener(FBEvent.SUCCESS, statusUpdated);
+ // TODO (keito): At the moment, we only support pic uploading if we're
+ // a user, not a page
+ if (droppedPhotos.length > 0) {
+ FBConnect.dispatcher.dispatchEvent(
+ new FBEvent(FBEvent.ALERT, "Uploading Photos"));
+
+ if (droppedPhotos.length > 1) {
+ // If we are uploading multiple photos, create album first
+ if (albumNameInput.length == 0) {
+ // Enforce album name
+ Application.application.focusManager.setFocus(albumNameInput);
+ return;
+ }
+ FBAPI.callMethod("photos.createAlbum", {
+ name: albumNameInput.text,
+ description: submittedMessage
+ }).addEventListener(FBEvent.SUCCESS, uploadDroppedPhotos);
+ } else {
+ // Just one photo; dump it into the app album
+ var photoCallArgs:Object = {
+ caption: submittedMessage
+ };
+
+ // TODO (keito): Do some resizing logic (to reduce bandwidth. but
+ // that also reduces quality, since it'll be resized/recompressed
+ // first on the client, then on the server)
+ FBAPI.uploadPhoto(droppedPhotos[0].file, photoCallArgs)
+ .addEventListener(FBEvent.SUCCESS, photoUploadSuccess);
+ droppedPhotos.removeItemAt(0);
+ }
+ } else {
+ FBAPI.callMethod("stream.publish", publish_options)
+ .addEventListener(FBEvent.SUCCESS, statusUpdated);
+ }
}
}
+ // Called when there are multiple photos to be uploaded and in response to
+ // a successful album creation.
+ private function uploadDroppedPhotos(event:FBEvent):void {
+ if (!event.data.hasOwnProperty('aid')) {
+ Output.bug("We didn't get back the album ID");
+ return;
+ }
+
+ var photoCallArgs:Object = {
+ aid: event.data.aid
+ };
+
+ for (var i:int = 0; i < droppedPhotos.length; i++) {
+ // TODO (keito): Do some resizing logic (to reduce bandwidth. but
+ // that also reduces quality, since it'll be resized/recompressed
+ // first on the client, then on the server)
+ FBAPI.uploadPhoto(droppedPhotos[i].file, photoCallArgs)
+ .addEventListener(FBEvent.SUCCESS, multiPhotoUploadSuccess);
+ remainingUploads++;
+ }
+ droppedPhotos.removeAll();
+ }
+
// Called when the user is done hopefully validating our pages
// extended permission.
private function pagePermissionChange(event:FBEvent):void {
@@ -139,10 +282,150 @@
// This just resets things to go
private function resetFields():void {
submittedMessage = null;
- composerInput.text = "";
+ composerInput.text = composerInput.focusOutText;
composerInput.editable = true;
shareButton.enabled = true;
FBConnect.dispatcher.dispatchEvent(new FBEvent(FBEvent.ENABLE));
}
+
+ /**
+ * Called when a photo in a single-photo upload operation succeeeds
+ */
+ private function photoUploadSuccess(event:FBEvent):void {
+ resetFields();
+
+ // Fake an addition to the stream
+ // TODO (keito): This is inherently sketchy, because we don't know how the
+ // albums will be aggregated in the stream. We should probably remove any
+ // existing mentions to the same album (app default album) in our Nile
+ dispatchEvent(new FBEvent("statusUpdated", {
+ post_id:null,
+ actor_id:NileContainer.FilterIsPage ?
+ NileContainer.CurrentFilter : FBConnect.session.uid,
+ app_id:null,
+ message:submittedMessage,
+ likes:{count:0, user_likes:false, can_like:true},
+ comments:{count:0, posts:[], can_post:true, can_remove:true},
+ created_time:(new Date().time / 1000),
+ is_page:NileContainer.FilterIsPage,
+ filter_key:NileContainer.CurrentFilter,
+ attachment:{
+ name:APP_DEFAULT_ALBUM_NAME,
+ media:[{
+ alt:submittedMessage,
+ href:event.data.href,
+ src:event.data.src,
+ type:"photo"}]
+ }
+ }));
+
+ FBConnect.dispatcher.dispatchEvent(new FBEvent(FBEvent.RESOLVED));
+ }
+
+ /**
+ * Called when a photo in a multi-photo upload operation succeeds
+ */
+ private function multiPhotoUploadSuccess(event:FBEvent):void {
+ // Don't do anything until all photos have been uploaded
+ remainingUploads--;
+ if (remainingUploads > 0) return;
+
+ resetFields();
+
+ // Direct users to album edit screen
+ if (!event.data.hasOwnProperty("aid"))
+ Output.bug("Album ID was not returned by server");
+ else {
+ navigateToURL(new URLRequest(URL_EDIT_ALBUM +
+ StringUtil.lowOrder64(event.data.aid)));
+ }
+ }
+
+ // Called when the user drags something over the box
+ private function dragEnter(event:NativeDragEvent):void {
+ // Make sure files are being dragged over
+ // TODO (keito): At the moment, we don't support drag-drop on pages
+ if (event.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT) &&
+ !NileContainer.FilterIsPage) {
+ NativeDragManager.acceptDragDrop(composerInput);
+ }
+ }
+
+ // Called when the user drops something onto the box
+ private function dragDrop(event:NativeDragEvent):void {
+ var files:Array = event.clipboard.getData(
+ ClipboardFormats.FILE_LIST_FORMAT) as Array;
+
+ for each (var photo:File in files) dropQueuedPhotos.push(photo);
+
+ // Make the dropped photo list appear
+ if (droppedPhotos.length == 0) {
+ droppedPhotoList.includeInLayout = true;
+ droppedPhotoList.visible = true;
+ photosAppear.play();
+ composerInput.focusOutText = FOCUS_OUT_CAPTION;
+ } else
+ addQueuedPhotosToGrid();
+
+ if (dropQueuedPhotos.length + droppedPhotos.length > 1)
+ composerInput.focusOutText = FOCUS_OUT_CAPTIONS;
+ }
+
+ // Called when the delete button is pressed on a photo
+ private function deletePhoto(event:MouseEvent):void {
+ if (droppedPhotoList.selectedIndex > -1) {
+ droppedPhotos.removeItemAt(droppedPhotoList.selectedIndex);
+ }
+ }
+
+ /**
+ * Called after the photo grid has finished appearing or just after photos
+ * have been dropped to add the photos to the grid. This is so that we don't
+ * start rendering the ItemRenderers until after the appear effect has
+ * finished, to prevent ugliness.
+ */
+ private function addQueuedPhotosToGrid():void {
+ if (dropQueuedPhotos.length > 0) {
+ for each (var photo:File in dropQueuedPhotos) {
+ // We must preload the image sometime before the request
+ photo.load();
+ droppedPhotos.addItem(new ObjectProxy({
+ file: photo,
+ source: photo.url
+ }));
+ }
+ dropQueuedPhotos = [];
+ }
+
+ if (droppedPhotos.length > 1) {
+ albumName.visible = true;
+ albumName.includeInLayout = true;
+ }
+ }
+
+ /**
+ * Called after the photo grid has finished disappearing
+ */
+ private function photosFinishedDisappearing():void {
+ droppedPhotoList.includeInLayout = false;
+ droppedPhotoList.visible = false;
+ albumName.visible = false;
+ albumName.includeInLayout = false;
+ albumNameInput.text = '';
+ composerInput.text = composerInput.focusOutText;
+ composerInput.focusOutText = FOCUS_OUT_STATUS;
+ }
+
+ /**
+ * Called when the photo grid is repainted. Hides it if appropriate.
+ */
+ private function repaintPhotoList():void {
+ if (droppedPhotoList.includeInLayout && droppedPhotos.length == 0
+ && dropQueuedPhotos.length == 0 && !photosDisappear.isPlaying) {
+ // Currently visible and need to disappear
+ photosDisappear.play();
+ }
+ }
]]></mx:Script>
-</mx:HBox>
+
+</mx:VBox>
View
38 fbair/composer/DroppedPhotosRenderer.mxml
@@ -0,0 +1,38 @@
+<!--
+ Copyright Facebook Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
+ <mx:Image id="photo"
+ source="{data.source}"
+ maxHeight="{this.height}"
+ maxWidth="{this.width - 10}"
+ left="5"
+ updateComplete="updateComplete()" />
+ <mx:Button id="closeButton"
+ styleName="storyDeleteButton"
+ click="deleteFunction(event)" />
+ <mx:Script>
+ <![CDATA[
+ public var deleteFunction:Function;
+
+ private static const CLOSE_BUTTON_PADDING:int = 4;
+
+ private function updateComplete():void {
+ closeButton.x = photo.contentWidth - 8 - CLOSE_BUTTON_PADDING;
+ closeButton.y = CLOSE_BUTTON_PADDING;
+ }
+ ]]>
+ </mx:Script>
+</mx:Canvas>
View
8 fbair/nile/renderers/NileRenderer.mxml
@@ -267,6 +267,14 @@
// Add to content box
if (contentBox) contentBox.addChild(content);
}
+
+ // Disown photos that we uploaded, because stream.get doesn't let us
+ // delete stream items that have been created due to us calling
+ // photo.upload
+ if (data.app_id == ApplicationBase.AppID && data.attachment.media is Array
+ && data.attachment.media[0].type == 'photo') {
+ data.app_id = null;
+ }
}
private function get hasRealAttachment():Boolean {
View
19 fbair/styles/composer.css
@@ -10,3 +10,22 @@ Composer {
paddingTop: 5;
verticalAlign: bottom;
}
+.droppedPhotoList {
+ paddingTop: 5;
+ paddingLeft: 5;
+ paddingBottom: 5;
+ paddingRight: 5;
+ rollOverColor: #FFFFFF;
+ selectionColor: #FFFFFF;
+}
+.albumFieldLabel {
+ color: #333;
+ font-size: 11;
+}
+.albumFieldText {
+ color: #000;
+ font-size: 11;
+ borderSkin: Embed(source='../assets/composer_textarea.png',scaleGridTop='2',scaleGridLeft='2',scaleGridBottom='4',scaleGridRight='4');
+ focusAlpha: 0;
+ focusSkin: ClassReference('mx.skins.ProgrammaticSkin');
+}
View
11 fbair/util/display/GrowableTextArea.as
@@ -31,8 +31,8 @@ package fbair.util.display {
[Bindable] public var disabledColor:uint = 0x808080;
[Bindable] public var focusOutHeight:int = 25;
[Bindable] public var focusInHeight:int = 40;
- [Bindable] public var focusOutText:String = "Write a comment...";
+ private var _focusOutText:String = "Write a comment...";
private var _active:Boolean = false;
public function GrowableTextArea() {
@@ -55,6 +55,15 @@ package fbair.util.display {
updateState();
}
+ // Setting/getting focusOutText
+ [Bindable] public function get focusOutText():String {
+ return _focusOutText;
+ }
+ public function set focusOutText(to:String):void {
+ if (text == _focusOutText || text == '') text = to;
+ _focusOutText = to;
+ }
+
// Update our settings based on active and all our vars
private function updateState():void {
setStyle("color", active ? enabledColor : disabledColor);
Please sign in to comment.
Something went wrong with that request. Please try again.