Jump to content

Adding a count to a basic player with mysql and php


Go to solution Solved by gizmola,

Recommended Posts

Hello everyone. Today I'm working on adding a counter to my working mp3 player (count each time a song is played). Right now it successfully grabs the id and plays the correct song without issue.
Now, I have added to my db "pcount" (for plays count). I looked around google and some other forums but all I can find is how to add a complete player, or how to add view counts to a different kind of music player completely (that I don't understand)
This is meant to be basic and easy. Below is what I have. I'm only including the parts that are relevant. Any ideas?

 

$sql_l = "SELECT * FROM audio";

<source src="audio/'.$row["audio"].'" type="audio/mpeg">
Your browser does not support the audio element.
</audio>

 

Not sure what you post is supposed to show us.  The code sample makes no sense since it is incomplete(?) and disjointed.  You show us a query. You show us an html source(?) tag.  You show us an ending audio tag.

Where's the part that is supposed be trying to add a counter to a table?  Where is the part that is supposed to populate the source tag with a query result?  Where is the fetch for that query result?

@ginerjm I didn't show the rest because on some sites I get fussed at if I show too much. If I show too little then I get fussed at. So I only show the parts that I am having trouble with. If I showed all the stuff that is working then what would be the point of that?

Know that the player is working. It plays the songs. A person uploads a song, it goes to the folder, the database stores the link, then the player catches the link and people can play the song.
I only need help with getting a count to the db column "pcount" when people click on play. Other than that there's nothing to show. If you REALLY want to see the rest of the pages script then sure I can comment with that too. But I don't want to be fussed at for putting in too much since I posted what is relevant.

Edit: you ask "where is this and that"... if I had that stuff for this task then I would have posted that. I don't have it, because I don't know how to add it. Which is the entire purpose of me asking for help

Edited by PNewCode

@ginerjm Here is the entire page. Please don't ask me "Why did you do this and that". I just need help getting the counter because everything else is working. And as long as it's working, I don't care about the 1,000 different ways I should have done it, please.

 

<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
  box-sizing: border-box;
}

#myInput {
  background-image: url('searchicon.png');
  background-position: 10px 10px;
  background-repeat: no-repeat;
  width: 100%;
  font-size: 16px;
  padding: 12px 20px 12px 40px;
  border: 1px solid #ddd;
  margin-bottom: 12px;
}

#myTable {
  border-collapse: collapse;
  width: 100%;
  border: 1px solid #ddd;
  font-size: 18px;
}

#myTable th, #myTable td {
  text-align: left;
  padding: 12px;
}

#myTable tr {
  border-bottom: 1px solid #ddd;
}

#myTable tr.header, #myTable tr:hover {
  background-color: #330000;
}
</style>
</head>
<body>
<font face="Verdana, Arial, Helvetica, sans-serif"> 


<script>
document.addEventListener('contextmenu', event => event.preventDefault());
</script>


<style type="text/css">
body {background:none transparent;
}
</style>

<style>
img {
  border: 2px solid #330000;
}
</style>

<input type="text" id="myInput" onkeyup="searchTable()" placeholder="Search by Band or Song Title" title="Type in a name">
</font> 
<table id="myTable" cellpadding="0" cellspacing="0">
  <tr class="header"> 
    <th > 
      <center>
        <font face="Verdana, Arial, Helvetica, sans-serif" color="#CCCCCC">Artist</font> 
      </center>
    </th>
    <th > 
      <center>
        <font face="Verdana, Arial, Helvetica, sans-serif" color="#CCCCCC">Cover</font> 
      </center>
    </th>
    <th ><font face="Verdana, Arial, Helvetica, sans-serif"> 
      <center>
        <font color="#CCCCCC"> Song Title </font> 
      </center>
      </font></th>
    <th ><font face="Verdana, Arial, Helvetica, sans-serif"> 
      <center>
        <font color="#CCCCCC">Song</font> 
      </center>
      </font></th>
    <th ><font face="Verdana, Arial, Helvetica, sans-serif"> 
      <center>
        <font color="#CCCCCC">Plays</font> 
      </center>
      </font></th>
    <th ><font face="Verdana, Arial, Helvetica, sans-serif"> 
      <center>
        <font color="#CCCCCC">Music Video</font> 
      </center>
      </font></th>
    <th ><font face="Verdana, Arial, Helvetica, sans-serif"> 
      <center>
        <font color="#CCCCCC">Uploaded By</font> 
      </center>
      </font></th>
  </tr>
  <?php 
  
  $hostname_l = "db stuff";
  $username_l = "db stuff";
  $password_l = "db stuff";
  $dbname_l = "db stuff";
  
   $conn_l = mysqli_connect($hostname_l, $username_l, $password_l, $dbname_l);
  if(!$conn_l){
    echo "Database connection error".mysqli_connect_error();
  }

$user_id = 151;
  $sql_l = "SELECT * FROM audio";
  
  


$result = $conn_l->query($sql_l);








if ($result->num_rows > 0) {
  // output data of each row
  while($row = $result->fetch_assoc()) {
      
    echo '<tr background="faded.png">
    <td><center><font face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"><b>'.$row["band"].'</b></font></center></td>
    <td><center><img src="img/'.$row["cover"].'" style="width:75px"></center></td>
    <td><center><font face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"><b>'.$row["audio_title"].'</b></font></center></td>
    <td><center><audio controls controlsList="nodownload">

<source src="audio/'.$row["audio"].'" type="audio/mpeg">
Your browser does not support the audio element.
</audio>

</center></td>
<td><center><font face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"><b>'.$row["pcount"].'</b></font></center></td>


<td><center><font face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"><b><a href="'.$row['video'].'" target="_blank">'.$row["video"].'</a></b></font></center></td>

<td><center><font face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"><b>'.$row["fname"].'</b></font></center></td>
  </tr>';
  
 
  
  }
} else {
  echo "0 results";
}


  
  ?> 
</table>
<font face="Verdana, Arial, Helvetica, sans-serif"> 
<script>
function searchTable() {
    var input, filter, found, table, tr, td, i, j;
    input = document.getElementById("myInput");
    filter = input.value.toUpperCase();
    table = document.getElementById("myTable");
    tr = table.getElementsByTagName("tr");
    for (i = 0; i < tr.length; i++) {
        td = tr[i].getElementsByTagName("td");
        for (j = 0; j < td.length; j++) {
            if (td[j].innerHTML.toUpperCase().indexOf(filter) > -1) {
                found = true;
            }
        }
        if (found) {
            tr[i].style.display = "";
            found = false;
        } else {
            tr[i].style.display = "none";
        }
    }
}
</script>
</font> 
</body>
</html>

 

Not directly related to your problem, but your opening and closing HTML tags should match

image.png.92c29a8d34e6cb0e6e20b2296a760b68.png

The opening tag should be <audio ...

An audio element has a "play" event which you could use to trigger updates to your count via AJAX.

Your html markup is antiquated. You should use a learining resource that was written this century and not in the days of Netscape Navigator.

@Barand Thank you. I didn't even notice that the tags weren't matched haha. I wonder how it's working like that then. Good learning point, thank you. So I should look for an ajax script that I can edit and learn how to use it to add the count to the database column?

13 minutes ago, PNewCode said:

I wonder how it's working like that then

I had another look at the code. You have an <audio opening tag. "<source" shouldn't be there.

Your ajax needs to use the player's "onplay" event.

@Barand Thank you. I searched every way of phrasing a counter for this I can think of, and putting "ajax" in the search string and I cannot find a single thing that shows anything like it with the type of media play that I'm using. Is there a term or something I'm missing for my searches? Or rather, since you don't know all of what I've tried, is there something more specific I can look for? I'm not asking anyone to write it for me, but I can't find anything even remotely suitable that will work with my player. Many thanks

Edit: Even more so, how to get it to save in the database or anything to save in the database with a click and ajax or javascript

Edit 2: Side note... the amount of results about Ajax liquid dish soap is hilarious

Edited by PNewCode

YOu didn't mention that you were using Ajax for this.  Why not just let the script upload the data, do the db update and then return to the player or whatever started this?

And then show us the Code that is not working , not all the html and css stuff.  Just the php.  And maybe the form so we can confirm that you are using named fields properly.

Hopefully this will simplify the whole ajax thing for you.

  • 1st create a php script that will accept a POST request with a method to identify which row in the audio table needs to be updated
    • This script needs to make a database connection and then issue an UPDATE query for that row
      • The query is simple enough, something like:  'UPDATE audio SET pcount = pcount + 1 WHERE id = ?'
      • You should use a prepared statement for this, where you bind the id parameter
      • After the update, you can add a query to get the new count if you would like, and return that in a json form using json_encode.
    • You will pass/get the ID parameter from the $_POST

 

You can test this script independently using any testing tool that can generate a post.  Postman is a good one, but if you use VSCode, then Thunderclient is an extension that will do the same thing, only integrated into VSCode, which is a nice option.

 

Once you've tested your update script (which you might want to call something like "update_audio_playcount.php")  you are ready to call this script via "ajax".  Ajax is simply some javascript code you can call when your play button is pushed.  You will need to populate your markup in some way so that you can get the audio row id value.

Typically people use 'data-*' attributes to do this, so a good way of handling this would be to make sure the play button has an attribute like "data-song-id=x" which should make it very easy from javascript to get the id of the row in the audio table you need to update.

Just to keep this simple, you can use the javascript fetch api to make your POST request to the update script.

This example is from the MDN page I linked with a couple of modification hints you need for a form POST:

// Example POST method implementation:
async function postData(url = '', data = {}) {
  // Default options are marked with *
  
  const formData = new FormData();
  formData.append('id', ... get the audio table id here and pass it);
  
  const response = await fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
      'Content-Type': 'application/json'
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: formData
  });
  return response.json(); // parses JSON response into native JavaScript objects
}

postData('https://example.com/answer', {})
  .then((data) => {
    console.log(data); // JSON data parsed by `data.json()` call
  });

 

The important detail we don't have is the structure of the audio table.  Hopefully each row has an auto_increment id value, so that you can use that to identify the row that's needed.  

@gizmola and @Barand Thank you both for that useful bunch of information. I was able to learn from that. And this is what I ended up with that works near-perfect. The only problem I have now, that I didn't anticipate being an issue, is that when the player his PAUSE and then PLAY again, it adds to the play count. So if someone wanted to make it look like they are getting a bunch of plays, they can just hit PAUSE - PLAY - PAUSE - PLAY over and over again. I wonder could there be a way around that?

 

$('audio').on("play", function(id,data, status){
    
    
    var a_id = $(this).attr("id");


    $.ajax({url: "count-play.php?id=" + a_id, success: function(result){
     $("."+ a_id + "count").html(result);
    }});

 

1 hour ago, PNewCode said:

I wonder could there be a way around that?

Keep track of whether or not you've sent the count request, and only send it if you haven't.

$('audio').on("play", function(){
    let requestSent = false
    return function(){
        if (requestSent){
            return;
        } 
        
        requestSent = true;
        let a_id = $(this).attr("id");
        $.ajax({
            url: "count-play.php?id=" + a_id
            , success: function(result){
                $("."+ a_id + "count").html(result);
            }
        });
    };
}());

The first time the event fires, requestSent will be false so the ajax call will run and record the play event and requestSent will be set to true.  Later events will see that requestSent is true and immediately return thus doing nothing.

 

  • Like 2

@kicken Thank you, however that seems to be preventing any count at all. What you have certainly stops the count every time play/pause is clicked. But it also doesn't allow new plays to be entered either. Any thoughts?

Edit: I made a test account and opened it in a new browser (one is chrome, the other is edge) to test it

Edited by PNewCode

If you have multiple audio tags or you're re-using the same tag for different tracks then you'll need to extend the code from using a simple true/false variable to something that can differentiate between the tags/tracks (ie, by the ID for example). An example would be to change requestSent into an array and store the ID of the played tracks into that array.  To test if the track has been played, search that array for the ID.

Edited by kicken
  • Solution

Yes, so implementing kicken's suggestion, this should work probably:

$('audio').on("play", function(){
    let requestSent = []
    return function(){
        let a_id = $(this).attr("id")
        if (requestSent.includes(a_id)) {
            return;
        } 
        
        requestSent.push(a_id)   
        $.ajax({
            url: "count-play.php?id=" + a_id
            , success: function(result){
                $("."+ a_id + "count").html(result)
            }
        });
    };
}());

Hopefully you understand what this code does, which makes use of some tricky javascript concepts, namely:

  • Javascript closure
  • An IFFE
  • Like 1

@gizmola You nailed it. I love this site. So much help and great learning experiences too. This is doing exactly as I needed.
So, just so I understand it (correct me if I'm wrong please, I want to learn from what is shown to me), for my education...


Like what @kicken said, the "let requestSent" tracks if the count request was sent, and if it has then it sends it in the "return" function for the player when PLAY button is clicked.
And then...
it looks for the id of the strack to include it in the functions request.
AND THEN WHAT YOU SAID....
Sends the ajax functions to complete the requests
Right?

I may be talking in circles and not using the correct words or phrases in my understanding, so I'm kind of saying this in lamens terms haha.

I'm excited now. I also have (on the same page) a like / unlike feature that sends a number to add to the database and displays the number. But it's annoying right now because it refreshes the whole page to show it. So with a longer list, it just goes to the top of the page when the "heart" is clicked on. So I'm going to try to use this to do the same for that too and see if I can pull it off.

YOU ALL ROCK SO MUCH! Thank you!

Edited by PNewCode

So just to say it, the on event handler is accepting a callback function to run when there is a "play" event.

A simpler solution would be to just have a function defined there, that the callback would run, or to define a function globally and pass the name of the function.

However, @Kicken coded this function to return an anonymous function.  It helps to focus in on return statements in code like this.

If you notice the requestSent variable is declared outside the function declaration that does the work.   This creates a "closure" (or takes advantage of javascript closure) depending on how you want to think about it.   It makes the variable requestSent available to the inner function that is being returned, and this variable will continue to exist in the browser's memory associated with the window/page, until such a time as a new request is made that causes new html/javascript/css to be loaded.  An alternative would be to declare requestSent globally and use that, but he gave you something more sophisticated -- a function that returns a function and takes advantage of a variable that is only visible to the anonymous function, and yet is available to the anonymous function across executions.

Each time the callback is run, this could be either for the same song or a different song, so inside the function, there is a jQuery call to find the id of the button.  

let a_id = $(this).attr("id")

It's good to think about why this is declared inside the function and how that works.  Since this handler can be called for any song, the $(this) resolves in this situation as the song that is being played.  Thus the a_id gets set each time there's a play event, and then gets the html id attribute.

I added code to push the value onto the requestSent array, which again, since it's part of the closure for the anonymous function, survives across plays. I  used Array.includes() to check if the song id already exists in requestSent.  If not, I update requestSent with requestSent.push(a_id) and the ajax runs, passing a_id.

The ajax is also being done using the jQuery library.

The final question you should probably be asking is:  if this is a function that returns a function, then how is it, that the callback, which requires a function to run, gets the actual function it needs.  A function that returns a function is not a callback.

The answer is that again Kicken used an IFFE here.   What is actually being passed is a function that is immediately executed.   You can see this because after the function definition

 

function () {
 ...
}

It is immediately followed by the parens ie.  () which causes javascript to execute the function.

function () {
 ...
}()

So this code works because the function that returns a function, is run immediately, giving the callback parameter what it wants ...   a function to run when a play event occurs.  The function is anonymous and only bound to the event handler for play events, which also keeps global scope from being cluttered with a symbol table entry for a function that is only needed for the callback.

The benefit of doing it this way is that he did not need to utilize a global variable,  since closure takes care of this for you.  This type of code is favored in many situations, since you don't have a slew of global variables floating around.  Nothing outside the callback function can see or modify the requestSent array -- yet it is essentially a private environment that the callback uses.

As I said previously -- advanced javascript stuff, that can be confusing if you are still learning javascript.

Hope this helps -- using those terms (IFFE, javascript closure, js anonymous function, js callbacks, js this) will lead you to an enormous amount of additional material if you need to explore them further.  

 

  • Like 1
  • Great Answer 1

@gizmola Wow thank you very much! I opened the working script and was following it down the line while reading your reply. It's all making more sense to me. And I was able to use that to create the other task that I was working on to add a count to something else on my page too.

Also, I'm going to use this same lesson to make a voting up and down for some fun polls on songs too! Thank you again!

I took an alternative approach, using two data attributes for each audio player element...

  • data-id : as above contains the track id
  • data-play : initially 0, set to 1 when play is clicked, reset to 0 when track has ended. The play count can only be incremented if play is clicked when data-play is 0. This prevents play...pause...play...pause from boosting the count.

The AJAX processing

  • receives the id,
  • increments pcount for that track,
  • retrieves the new pcount,
  • returns that in the response
<?php
    include 'db_inc.php';       //   use your own
    $pdo = pdoConnect('db2');   //   db connection code

################################################################################
#  Handle ajax requests                                                        #
################################################################################
    if (isset($_POST['ajax']))  {
        
        if ($_POST['ajax']=='addPlay') {                                        // increment the count for the selected player file
            $stmt = $pdo->prepare("UPDATE audio                                 
                                     SET pcount = pcount + 1
                                   WHERE id = ?
                                  ");
            $stmt->execute([ $_POST['id'] ]);                                   // get the new count
            $res = $pdo->prepare("SELECT pcount                                 
                                  FROM audio
                                  WHERE id = ?
                                  ");
            $res->execute([ $_POST['id'] ]);
            $pc = $res->fetchColumn();
            exit("$pc");                                                        // send the new count as the reponse
        }
    }
    
################################################################################
#  get available audio files and build page output                             #
################################################################################
    $res = $pdo->query("SELECT id
                             , audio
                             , pcount
                        FROM audio     
                       ");
    $players = '';
    foreach ($res as $row) {
       
            $players .= "<tr>
                             <td>" . substr(basename($row['audio']), 0, -4) . "</td>
                             <td>
                                 <audio class='player' data-id='{$row['id']}' data-play='0' 
                                    controls controlsList='nodownload' 
                                    src='{$row['audio']}'  preload>
                                        Your browser does not support the audio element.
                                 </audio>
                             </td>
                             <td data-id='{$row['id']}'>{$row['pcount']}
                         </tr>
                         ";
        
    }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>sample</title>
<meta charset="utf-8">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script type='text/javascript'>

    $().ready(function() {
        $(".player").on("play", function() {
            let id = $(this).data("id")
            if ($(this).attr("data-play")=='0') {                               // only allow increment when data-play is 0
                $.post(
                    "",
                    {"ajax":"addPlay", "id":id},
                    function(resp) {
                        if (resp > '0') {                                       // update the page with new play count
                            $("td[data-id="+id+"]").html(resp)
                            
                        }
                    },
                    "TEXT"
                )
                $(this).attr("data-play", '1')                                  // set data-play to 1 to prevent increments
            }    
        })
        
        $(".player").on("ended", function() {
            $(this).attr("data-play", '0')                                      // now track has ended, reset data-play to 0
        })
        
    })

</script>
</head>
<body>
    <table border='1'>
        <tr><th>File name</th>
            <th>Player</th>
            <th>Plays to<br>date</th>
        </tr>
        <?= $players ?>
    </table>
</body>
</html>

 

  • Like 1

@kicken @gizmola @Barand and others,

EDIT!!!
I solved it. I'm adding the solution to the bottom in case anyone else can use this for help. I am still crediting you all for this because like I said, it stems from what I learned on here on this topic.



So I took what I learned from this and made a like / dislike feature. I am stuck again (of course) but I NEARLY have it. I'm adding it to this because it stems from this topic (if this should be a new question let me know and my apologies)

So the below script is including a calandar bit that I have. There's no issues with that. Also, it has a thumbs up bit that is working perfectly too. Adding the functions for the thumbs down isn't agreeing with me though.

WHAT I HAVE DONE:
Lots of stuff haha. But The part at the bottom I tried duplicating the whole thing and changing the parts to reflect the php pages and database (heart2, addheartno.php, count2)
When I have that duplicated in a whole part of it's own on a DIFFERENT page, it works to duplicate it, however the calandar parts aren't on that different page. So I'm guessing that I'm missing something
(Please see the second script to see what I tried and got closest but no cigar)


This is what I have that works with just the thumbs up

  <script>
  $( function() {
    $( "#datepicker" ).datepicker({changeMonth: true,
    changeYear: true});

  } );

		init();

	function init() {
		var today = new Date();

var date = (today.getMonth()+1)+'/'+today.getDate() +'/'+ today.getFullYear();

		$.get("getplaylist-playhouse.php",
        {
            cur_date: date
        },
        function(data, status){

            let element = document.getElementById("play_list");
			while (element.firstChild) {
			  element.removeChild(element.firstChild);
			}

			document.getElementById("play_list").innerHTML = data;

        });
	}
	$(document).on("change", "#datepicker", function () {

         cur_date =$(this).val();
		$.get("getplaylist-playhouse.php",
        {
            cur_date: cur_date
        },
        function(data, status){

            let element = document.getElementById("play_list");
			while (element.firstChild) {
			  element.removeChild(element.firstChild);
			}

          
 /////////// THIS IS THE SECTION THAT HANDLES THE THUMBS UP //////////
          
          
			document.getElementById("play_list").innerHTML = data;
			$('.heart').on("click", function(){
    

    var a_id = $(this).attr("id");

    $.ajax({url: "addheart.php?id=" + a_id, success: function(result){
     $("#count"+ a_id).html(result);
    }});

    
    
})

        });


    })
  </script>

 

And this is what I tried to add the thumbs down, and doesn't work (only showing what I added to it)
And by the way, what I mean by it working on the other page, I'll show the complete script below this one
 

document.getElementById("play_list").innerHTML = data;
			$('.heart').on("click", function(){

var a_id = $(this).attr("id");

    $.ajax({url: "addheart.php?id=" + a_id, success: function(result){
     $("#count"+ a_id).html(result);
    }});

    
    
})



document.getElementById("play_list").innerHTML = data;
			$('.heartno').on("click", function(){

var a_id = $(this).attr("id");

    $.ajax({url: "addheartno.php?id=" + a_id, success: function(result){
     $("#count2"+ a_id).html(result);
    }});

    
    
})

 

And this is what I have that is working on the other page, but doesn't work when I do this with the page that also has the calandar bit to it

 

<script>
$('.heart').on("click", function(){
    

    var a_id = $(this).attr("id");

    $.ajax({url: "addheart.php?id=" + a_id, success: function(result){
     $("#count"+ a_id).html(result);
    }});

    
    
})
</script>




<script>
$('.heartno').on("click", function(){
    

    var a_id = $(this).attr("id");

    $.ajax({url: "addheartno.php?id=" + a_id, success: function(result){
     $("#count2"+ a_id).html(result);
    }});

    
    
})
</script>

Any thoughts on what I'm missing?

 

SOLUTION!

			document.getElementById("play_list").innerHTML = data;
			$('.heart').on("click", function(){
    

    var a_id = $(this).attr("id");

    $.ajax({url: "addheart.php?id=" + a_id, success: function(result){
     $("#count"+ a_id).html(result);
    }});

    
    
})

			$('.heartno').on("click", function(){
    

    var a_id = $(this).attr("id");

    $.ajax({url: "addheartno.php?id=" + a_id, success: function(result){
     $("#count2"+ a_id).html(result);
    }});

    
    
})

        });


    })
  </script>

 

Edited by PNewCode
This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.