Building a Flex Multiple File Uploader with Image Preview

As the Internet integrates itself more and more into our daily lives, we see the popularity of electronic media spread like a virus. Websites like Youtube and Flickr have had a great deal of success by primarily focusing on user-generated content. Because so many people are uploading media, making the process as easy as possible makes the most sense. Keeping reading to see how Flex can do this for you.

 
 
 

All modern browsers have some default methods of uploading which can be expanded to some degree.  But to make a really sleek application which can provide cool features like upload progress, image preview, and multiple file uploads may take a good deal of work in DHTML.  Using Flex/Flash to implement our application also brings the added capability of filtering files based on their type, which is not possible with web standards at all. Supporting an app like this on multiple browsers on multiple operating systems increases the level of complexity.   Looking at the current technology options for creating a custom uploader leaves a lot to sift through.  With that said, I think a Flash Uploader sets itself apart by allowing customizable user interfaces that work well on any platform which supports the Flash player.

What Flex brings to the table

Flex is a technology that gives us the ability to write Rich Internet Applications (RIAa) using an XML-based language (known as MXML) and ActionScript.  It differs from Flash in that it is geared toward applications, not animations or games.  These latter items can be created with Flex, but using the right tool for the job is always a good idea.  Like Flash, Flex applications compile into an SWF.  The SWF file runs in a web browser or on a desktop. Suffice it to say, the Flash player is installed on 99% of the Internet-enabled computers, so I wouldn’t worry about users who can’t use this application.

It is important here to emphasis the XML factor of Flex. You could write this application using Flash, but Flex brings the ‘ease of use’ in creating a user interface and connecting data to it. Having a base set of common components at your disposal greatly decrease your development time, allowing you to get your product to market faster.

Flex applications are built with Flex Builder, which is a free download from www.adobe.com.  There are several resources available on the web to help get you started on your own applications.  I won’t cover the fundamentals of Flex Builder or Flex because it is out of the scope of this article. 

Setting up the UI

One of the main foci of our uploader will be to preview the image after the upload is complete.  This, along with having the ability to upload multiple files in succession, will dictate the type of UI we will build.  The Flex controls that lend themselves to this task most easily are the List-based controls.  In particular, because we need to display at least three types of information (the file name, the upload progress of the file, and the file image once successfully uploaded), a DataGrid is the most logical choice.  We will let Flex do a lot of the heavy lifting for us by making use of the "out of the box" controls.

With that said, let’s start by creating the MXML for a UI. Take a look at the code below:

<?xml version="1.0" encoding="utf-8"?>

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" 

           paddingTop="5"

           paddingLeft="5"

           paddingRight="5"

           paddingBottom="5"

           layout="vertical"

           creationComplete="init()">

    <mx:Panel width="100%" 

                   height="100%" 

                   title="Upload List" 

                   horizontalAlign="center">

        <mx:DataGrid id="fileList" width="100%" height="100%" dataProvider="{uploadQueue}">

            <mx:columns>

                <mx:DataGridColumn headerText="Filename" dataField="name"/>

                <mx:DataGridColumn headerText="Progress" dataField="progress"/>

                <mx:DataGridColumn headerText="Preview"  

                                              width="65"

                                              dataField="preview" 

                                              itemRenderer="mx.controls.Image">

                </mx:DataGridColumn>

            </mx:columns>

         </mx:DataGrid>

        <mx:ControlBar>

            <mx:HBox width="100%" horizontalAlign="center">

            <mx:ButtonBar horizontalGap="2" itemClick="buttonHandler(event)">

                <mx:dataProvider>

                    <mx:Object label="Select Files"/>

                    <mx:Object label="Start Upload"/>

                </mx:dataProvider>

            </mx:ButtonBar>

        </mx:HBox>

    </mx:ControlBar>

</mx:Panel>

Our code starts with the standard XML declaration followed by the Application tag. It is important to note that the underpinnings of Flex dictate the tags which are available to us. Each tag relates to a ActionScript Class. You can see this is action by holding down the Control-Key and clicking on the Application tag. The Flex IDE will open a new window showing you the ActionScript Class used to create the tag. The scope of this article is beyond that of an introduction to Flex . Visit the Abode Flex developer Center to get your feet wet:

http://www.adobe.com/devnet/flex/

For simplicity, I’ve decided to add some inline styling elements here rather than in a stylesheet.  Our Application tag sets the layout, and some padding.  This UI will be completely liquid, which will allow for the window resizing without a significant impact on the Flex application.

The next tag is a Panel.  The Panel provides a nice delineation of controls that relate to one another.  Notice we are setting the width and the height to 100%.  As stated early, this makes our layout change with the resizing of the browser.  We’ve given the Panel a title of "Upload List."

At the heart of the UI is the DataGrid.  Here is where we will display a list of files which are in a state of being uploaded, completely uploaded, or ready for upload.  For each file we will display its name, its progress or status, and a preview image for files that have been uploaded.  The columns tag and its descendants give us great flexibility in telling Flex what we want to appear and how we want it to appear.  

Of note in the DataGrid is the dropIn itemRenderer for column 3.  This field will make it easier for us to supply an image URL, which will appear in the DataGrid.  I won’t go into too much detail about the DataGrid, but you can find more information here:

http://livedocs.adobe.com/flex/3/html/help.html?content=dpcontrols_6.html

http://livedocs.adobe.com/flex/3/html/cellrenderer_3.html

Tying the data to our DataGrid is the uploadQueue ArrayCollection – specified as the dataProvider attribute.  Our ArrayCollection will be poplulated with the files selected by the user for upload.  Our application will continuously update this ArrayCollection to reflect its current state.

If you are not familiar with DataGrids and ArrayCollections, I recommend taking the time to explore these subjects before continuing.  

Lastly, you will see the ControlBar, which contains the ButtonBar.  The ControlBar visually complements the Panel and gives us the opportunity to add buttons on the bottom of our Panel.  For this application we will need two buttons: "Select Files" and "Start Upload."  The ButtonBar provides a nice uniform way of displaying buttons.  Unlike a regular button, the ButtonBar uses the itemClickEvent to take action when the user clicks on one of the buttons.  We specified a function called "buttonHandlers"  to take care of this task.  Here is what buttonHandler looks like:

private function buttonHandler(event:ItemClickEvent):void{

       

       switch(event.label){

       

           case ‘Select Files':

                  uploadList.browse(); 

           break;

          case ‘Start Upload':

                 uploadNextFile();

          break;

       }

}

Of course, the "buttonHandler" method is contained in our Script tag.  In this method we will initiate the selection of files for uploading and starting the upload process.  Notice that I’ve set up a switch statement for each button.  This allows easy expandability and customization of the ButtonBar.

Here is what our UI looks like:

 

{mospagebreak title=Selecting Files}

Flex makes the job of selecting single or multiple files easy.  Our aim here is to allow the user to click on the "Select Files" button and then select the files to upload.  Once the files are selected, the user can then use the "Start Upload" button to post the files to the server.

Let’s start by getting the files from the user.  We will be using the FileReferenceList class to accomplish this task.  Alternatively, if we only wanted to upload a single file, we would use the FileReference class.  

The first step is importing the necessary code:

// Imports

import mx.events.ItemClickEvent;

import flash.net.FileReferenceList;

The ItemClickEvent was already added for the ButtonBar.  The "import FileReferenceList" statement now gives us the ability to instantiate the object we need to retrieve the list of files from the user.  The next step is declaring a FileReferenceObject to use.  We’ll call this object uploadList.

// Properties

public var uploadList:FileReferenceList;

To create a new instance of this FileReferenceList, use the "new" operator like so:

// create an instance of the File Reference List

uploadList = new FileReferenceList();

To keep things nice and neat, I’ve created a method called "init" which is called once our application fires the creationComplete event.  Creating an init method is good for expandability.  I can now place any other initialization code in the same method.

The FileRefenceList class contains a method called browse, which will open a native File Dialog box and request that the user select mutilple files.  The dialog window is different for every operating system.  We can trigger this dialog in our buttonHandler method.  We already have a case for "Select Files" which will call ‘uploadList.browse()’.  The browse method takes over from this point.  I’m developing with Microsoft Vista, which has the following look:

 

Specifying File Types

We can call the browse method as we did above with no parameters.  This tells the native file selection dialog to allow the user to select any files they wish.  This may be good in some cases, but there are times where we want only certain file types to be allowed.  To remedy this, the browse method takes a parameter that limits the file types which can be selected.

The parameter is an array that contains FileFilter Objects.  Here is how we would use this:

// create a fileFilter – class declaration

private var imageTypes:FileFilter;

// set the file filter type – jpg/png/gif – init method

imageTypes = new FileFilter("Images (*.jpg, *.jpeg, *.gif, *.png)", "*.jpg; *.jpeg; *.gif; *.png");

             

// upload – buttonHandler

uploadList.browse([imageTypes]); 

  

For simplicity, I placed these lines of code in sequence.  As you will see from the final code below, they actually exist in different places.  Note here that I’m using the short form of an array [].

Calling browse and passing our array of fileFilters effectively removes all non jpg, png, and gif files from the file list.  

{mospagebreak title=Getting the Files}

Once the user selects their files, control is passed back to our Flex app.  The list of files is now contained in the uploadList object in a property called fileList.  One important thing to note here is how your application knows when the user has finished selecting files.  The FileReferenceList object uses the "select" event to notify the application that the user has made their selection.  So, we will need to catch this event and take action when the event is dispatched.  We do this by adding the following event listener:

uploadList.addEventListener(Event.SELECT,populateDataGrid);

This line of code tells the uploadList object to call the populateDataGrid method once the user has finished selecting files.  Our populateDataGrid method will take the file list and add the names to our datagrid.  Here is the populateDataGrid method:

private function populateDataGrid(event:Event):void{

   

   // remove any previous entries in the upload list

   uploadQueue.removeAll();

   

   // add all the new items

   for each( var file:FileReference in uploadList.fileList){

        uploadQueue.addItem({name:file.name,

                                       progress:’Ready’,

                                       preview:”,

                                       fileRef:file});

   }

}

The populateDataGrid function does two things:

1. Clear out the current list of items in the DataGrid.  This is done with the uploadQueue.removeAll() statement.  The uploadQueue is an ArrayCollection that we’ve set up for the DataGrid.  This will make it our lives easier when we begin uploading each file.  Remember, uploadQueue is the dataProvider for our DataGrid.

2. Add new data to the DataGrid.  We do this by updating our ArrayCollection with the items from the fileList.  The fileList property of our uploadList object is an array of objects of type FileReference.  We add this object to the ArrayCollection along with some other information for the DataGrid.  We could alternatively just add the FileReference Object to the ArrayCollection and NOT make it a property of the ArrayCollection Item.  I decided to keep it simple this time around.

Once the populateDataGrid method executes, our DataGrid will be populated.  We will then wait for our user to click the "Start Upload" button.

{mospagebreak title=Uploading}

Now that we have all of the information needed to upload our files, we can begin the process of uploading.  So far, in our application we’ve used the FileReferenceList to gather all the files selected by the user.  To actually upload the files we will need to use the FileReference class.  Although we can retrieve a list of files from the user, we still must upload them one at a time.

We must also consider the state of each file and code our upload application so that it knows when one upload is complete and to start a new one. As we are uploading each file we will need to present the progress to the user.  In short, there are a lot of  details to track.  With that said, a good starting place would be to talk about the alogrithm we intend to use.

Step 1: Find a file that needs to be uploaded.  In this step, we will traverse our uploadQueue ArrayCollection, searching for a fileRef that is ready to be uploaded.  We can use the "progress" property of the object to determine if the object is ready.  The "progress" property is set to "Ready" for all files before the upload starts.  If no items are found with a progress of "Ready," we simply do nothing.    

Step 2: Initiate the upload and wait.  Once an item is found to be ready to upload, start the upload.  To do this we will use the fileReference of the file and call its upload method.  Recall that the FileRefence object for each selected file is a property of the ArrayCollection item.  We can simply trigger the upload by calling the upload method of the fileReference.  Before we can do that, however, we need to examine more logic.

So far we’ve kept the code simple, but here is where things get a little more complicated.  Let’s talk about what happens when the user clicks the "Start Upload" button.  Here is a method called uploadNextFile(), which takes care of much of the logic involved in initiating uploads:

private function uploadNextFile():void{

   var file:FileReference;

  

   // get the next file from the queue

   for each(var o:Object in uploadQueue){

                    

                    

                    // if we find a ready status, then start the upload

                    if (o.progress=="Ready"){

                     // save the current object for updating

                     currentFile= o;

                    

                     // update the progress

                     o.progress="Initializing Upload";

                     uploadQueue.itemUpdated(currentFile); // force a refresh

                    

                     // grab the file reference

                     file = o.fileRef;

                    

                     // add event listeners

                     file.addEventListener(Event.COMPLETE, uploadComplete);

                     file.addEventListener(ProgressEvent.PROGRESS, uploadProgress);

                     file.addEventListener(IOErrorEvent.IO_ERROR, uploadError);

                    

                        // generate an ID for this upload

                        o.uploadId=Math.round(Math.random() * (5000 – 1000));

                        

                     // upload the file

                     uploadURL.url = uploadPath + o.uploadId ;

                     file.upload(uploadURL);

                    

                     return;

                    }    

                      

   }

   

  uploadQueue.itemUpdated(currentFile); // force a refresh

}

We start this function by declaring "file," which is our FileReference.  We will use this reference to point to the fileReference of an item of the uploadQueue ArrayCollection.

At the heart of this function is the for each loop, which iterates over all the items in our collection.  For every item in the collection we check for the value of progress.  If the progress is "Ready," we know we can begin the upload process for this item.  If the progress is not "Ready," we just iterate to the next item.

If the current item is "Ready," we set a variable called currentFile to point to the item currently being uploaded.  This will make it easier for other parts of the application to directly address the properties (i.e. upload progress, image preview) of the item being uploaded.  We then set the progress of the item to "Initializing," which the following code does:

// update the progress

o.progress="Initializing Upload";

uploadQueue.itemUpdated(currentFile); // force a refresh

The above code requires a bit of explanation.  Because we are updating an ArrayCollection which is bound to a DataGrid, we need to tell Flex that the ArrayCollection has been updated.  This will cause the DataGrid to be visually updated, which is important, because we want to be sure the user sees the progress of the upload.   By using the itemUpdated method of the  uploadQueue object, we can trigger this update.            

{mospagebreak title=Listening for Upload Events}

Although we have not initiated the upload to the server, we need to tell Flex what to do when certain things happen in the upload process.  The next important block of code in our uploadNext method is where we assign event listeners to our file reference. 

// add event listeners

file.addEventListener(Event.COMPLETE, uploadComplete);

file.addEventListener(ProgressEvent.PROGRESS, uploadProgress);

file.addEventListener(IOErrorEvent.IO_ERROR, uploadError);

Here we are assigning the uploadCompete, uploadProgress, and uploadError methods to events of upload complete, upload progress and upload error, respectively.  We will talk more about these in a bit.   For now, let’s get the upload started.  

Kicking Off the Upload:

Here are the last few lines of our method:

// generate an ID for this upload

o.uploadId=Math.round(Math.random() * (5000 – 1000));

                        

// upload the file

uploadURL.url = uploadPath + o.uploadId ;

file.upload(uploadURL);

One of the main goals of this application is to get a preview of the image after its been uploaded.  To implement this, we are going to use a simple method of sending an id number to the server along with the file.  We don’t need to be too strict at this point, so a random number will suffice as an id.  For a real world application you’ll want to use a more defined and secure method. To generate our id we use the Math.round method as shown above.  We append the id to our upload URL by appending it to the url property of the uploadURL object.

The uploadURL object is a required parameter of the upload method of the file object.  The best way to initialize this value is to create a constant, like so:

// Constants

public const imageUrl:String = "http://dev/flexFiles/";

public const uploadPath:String = "http://dev/flexUploader.php?id=";

Here is how we define uploadURL:

private var uploadURL:URLRequest;

and we assign it a value in out init method:

// set the upload URL

uploadURL = new URLRequest();

For each file being uploaded, we append our random number to the uploadPath string and set the uploadURL.url property to this value:

uploadURL.url = uploadPath + o.uploadId ; // http://myserver.com/flexUpload.php?id=123

When the server receives the id (via the GET superglobal), it renames the file using this number, thereby making it identifying to our application. Again, this is a simple demonstrative method of tracking uploads.  You should not use this method in a production environment. 

Finally, kick off the upload process with:

file.upload(uploadURL);

From this point on, we must rely on messages sent from Flex to tell us the progress of the upload, whether the upload is complete, or if there was a problem with the upload.  We use our listener functions as assigned above to take the appropriate action.

Step 3: Display the progress of the uploading file.  As the file is uploading, our uploadProgress method will be called.  Here is the code:

private function uploadProgress(event:ProgressEvent):void{

    currentFile.progress = event.bytesLoaded + " of " + event.bytesTotal;

    uploadQueue.itemUpdated(currentFile);

}

This method is pretty straightforward.  We are using our currentFile reference to set the progress property to the number of bytes uploaded.  The event passed to us contains this information.  We make sure the DataGrid is updated by calling the dataProviders itemUpdated method.  The uploadProgress method can be called several times during the upload process.

Step 4. Once the file upload is complete, display the preview  image and return to Step 1.  Once the upload of our file is compete, our uploadComplete method will be called.  Here is the code:

private function uploadComplete(event:Event):void{

    // Mark the upload as completed

    currentFile.progress="Complete: " + currentFile.progress;

    // set the uploaded image src

    currentFile.preview=imageUrl + 

                              currentFile.uploadId + "_" +

                              currentFile.fileRef.name;

    // find the next upload

    uploadNextFile();

}

This method first updates the status.  It prepends the byte count with the word "Complete:".  Now that the upload is complete, the image is available for display.  We can simply set the source of our dropIn itemRender to the path to the image, which we also defined as a constant:

public const imageUrl:String = "http://dev/flexFiles/";

This portion of our application requires a particular configuration on the web server.  In particular, our uploaded files must be available in the flexFiles directory.  Also, the server must save the files in this directory with the id pre-pended to it.  I’ll included the PHP upload script below.

Here is what a completed upload looks like:

 

{mospagebreak title=Error Event}

If an error occurs, our uploadError method will be called.  Here is the code:

private function uploadError(event:Event):void{

    

    currentFile.progress="Error!";

    uploadQueue.itemUpdated(currentFile); // force a refresh

    // find the next upload

    uploadNextFile();

}

In this method we just update the progress property to "Error!", update the DataGrid and look for the next upload.

PHP Upload Script

<?php

$MAXIMUM_FILESIZE = 1024 * 1000; 

if ($_FILES['Filedata']['size'] <= $MAXIMUM_FILESIZE)

{

    $move_result = move_uploaded_file($_FILES['Filedata']['tmp_name'], 

                                                    "./flexFiles/" . 

                                                    $_GET['id'] . "_" .

                                                    $_FILES['Filedata']['name']);

    error_log("moving file: " . $_FILES['Filedata']['tmp_name'],3,"/tmp/upload.log");

}else{

    error_log("file to large: " . $_FILES['Filedata']['tmp_name'],3,"/tmp/upload.log");

}

The upload script is very simple.  For our demonstration, we are not checking the number of files and user identity.  These steps, as well as other security precautions, should be taken before using this file in a product environment.  For more information on PHP uploading, see http://us3.php.net/manual/en/features.file-upload.php.

In Closing

In less than 200 lines of code, we were able to create a Flex uploader with image preview which runs on multiple platforms.  There are somes parts of the process which are a bit complicated, but with some study this article should provide an excellent guide to building your own custom uploader.  

This code can be furthered by making note of the ability of Flash Player 10’s FileReference Object which can read and write files locally to your computer. This can improve the efficiency of your application by allowing a preview of your images before uploading them to the server.

Here are some links that you may find interesting:

http://www.adobe.com/products/flex/

http://www.adobe.com/products/player_census/flashplayer/

http://livedocs.adobe.com/flex/3/html/help.html?content=dpcontrols_6.html

http://livedocs.adobe.com/flex/3/html/help.html?content=cellrenderer_3.html

http://livedocs.adobe.com/flex/3/html/help.html?content=17_Networking_and_communications_7.html

Complete Code:

<?xml version="1.0" encoding="utf-8"?>

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" 

           paddingTop="5"

           paddingLeft="5"

           paddingRight="5"

           paddingBottom="5"

           layout="vertical"

           creationComplete="init()">

<mx:Panel width="100%" 

     height="100%" 

     title="Upload List" 

     horizontalAlign="center">

<mx:DataGrid id="fileList" width="100%" height="100%" rowHeight="50"

        dataProvider="{uploadQueue}">

<mx:columns>

<mx:DataGridColumn headerText="Filename" dataField="name"/>

<mx:DataGridColumn headerText="Progress" dataField="progress"/>

<mx:DataGridColumn headerText="Preview"  

              width="65"

              dataField="preview" 

              itemRenderer="mx.controls.Image">

</mx:DataGridColumn>

</mx:columns>

</mx:DataGrid>

<mx:ControlBar>

   <mx:HBox width="100%" horizontalAlign="center">

    <mx:ButtonBar horizontalGap="2" itemClick="buttonHandler(event)">

   <mx:dataProvider>

   <mx:Object label="Select Files"/>

   <mx:Object label="Start Upload"/>

   </mx:dataProvider>

   </mx:ButtonBar>

   </mx:HBox>

</mx:ControlBar>

</mx:Panel>

<mx:Script>

<![CDATA[

// Imports

import mx.events.ItemClickEvent;

import flash.net.FileReference;

import flash.net.FileReferenceList;

import mx.collections.ArrayCollection;

import flash.net.FileFilter;

   // Constants

   public const imageUrl:String = "http://dev/flexFiles/";

   public const uploadPath:String = "http://dev/flexUploader.php?id=";

   

// Properties

public var uploadList:FileReferenceList;

private var uploadURL:URLRequest;

private var currentFile:Object;

private var imageTypes:FileFilter;

[Bindable] public var uploadQueue:ArrayCollection = new ArrayCollection();

public function init():void{

// create an instance of the File Reference List

uploadList = new FileReferenceList();

uploadList.addEventListener(Event.SELECT,populateDataGrid);

// set the upload URL

uploadURL = new URLRequest();

           // set the file filter type

           imageTypes = new FileFilter("Images (*.jpg, *.jpeg, *.gif, *.png)", "*.jpg; *.jpeg; *.gif; *.png");

             

             }

private function buttonHandler(event:ItemClickEvent):void{

       

       switch(event.label){

       

        case ‘Select Files':

          uploadList.browse([imageTypes]); 

        break;

       

        case ‘Start Upload':

          uploadNextFile();

        break;

       }

}

private function populateDataGrid(event:Event):void{

   // remove any previous entries in the upload list

   uploadQueue.removeAll();

   

   // add all the new items

   for each( var file:FileReference in uploadList.fileList){

    uploadQueue.addItem({name:file.name,

                        progress:’Ready’,

                        preview:”,

                        fileRef:file});

   }

}

private function uploadNextFile():void{

   var file:FileReference;

  

   // get the next file from the queue

   for each(var o:Object in uploadQueue){

                    

                    

                    // if we find a ready status, then start the upload

                    if (o.progress=="Ready"){

                     // save the current object for updating

                     currentFile= o;

                    

                     // update the progress

                     o.progress="Initializing Upload";

                     uploadQueue.itemUpdated(currentFile); // force a refresh

                    

                     // grab the file reference

                     file = o.fileRef;

                    

                     // add event listeners

                     file.addEventListener(Event.COMPLETE, uploadComplete);

                     file.addEventListener(ProgressEvent.PROGRESS, uploadProgress);

                     file.addEventListener(IOErrorEvent.IO_ERROR, uploadError);

                    

                        // generate an ID for this upload

                        o.uploadId=Math.round(Math.random() * (5000 – 1000));

                        

                     // upload the file

                     uploadURL.url = uploadPath + o.uploadId ;

                     file.upload(uploadURL);

                    

                     return;

                    }    

                      

   }

   

  uploadQueue.itemUpdated(currentFile); // force a refresh

}

private function uploadComplete(event:Event):void{

// Mark the upload as completed

currentFile.progress="Complete: " + currentFile.progress;

// set the uploaded image src

currentFile.preview=imageUrl + 

                   currentFile.uploadId + "_" +

                   currentFile.fileRef.name;

// find the next upload

uploadNextFile();

}

private function uploadProgress(event:ProgressEvent):void{

currentFile.progress = event.bytesLoaded + " of " + event.bytesTotal;

uploadQueue.itemUpdated(currentFile);

}

private function uploadError(event:Event):void{

currentFile.progress="Error!";

uploadQueue.itemUpdated(currentFile); // force a refresh

// find the next upload

uploadNextFile();

}

]]>

</mx:Script>

</mx:Application>



[gp-comments width="770" linklove="off" ]

chat sex hikayeleri Ensest hikaye