CC-1335 Plupload has been added to support multi-file upload.
This commit is contained in:
parent
71a97cdc93
commit
318337391c
9 changed files with 1035 additions and 575 deletions
19
src/modules/htmlUI/var/html/assets/jquery-1.3.2.min.js
vendored
Normal file
19
src/modules/htmlUI/var/html/assets/jquery-1.3.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/modules/htmlUI/var/html/assets/plupload/jquery.plupload.queue.min.js
vendored
Normal file
1
src/modules/htmlUI/var/html/assets/plupload/jquery.plupload.queue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/modules/htmlUI/var/html/assets/plupload/plupload.full.min.js
vendored
Normal file
1
src/modules/htmlUI/var/html/assets/plupload/plupload.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
177
src/modules/htmlUI/var/html/assets/plupload/plupload.queue.css
Normal file
177
src/modules/htmlUI/var/html/assets/plupload/plupload.queue.css
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
Plupload
|
||||
------------------------------------------------------------------- */
|
||||
|
||||
.plupload_button {
|
||||
display: -moz-inline-box; /* FF < 3*/
|
||||
display: inline-block;
|
||||
font: normal 12px sans-serif;
|
||||
text-decoration: none;
|
||||
color: #42454a;
|
||||
border: 1px solid #bababa;
|
||||
padding: 2px 8px 3px 20px;
|
||||
margin-right: 4px;
|
||||
background: #f3f3f3 url('../img/buttons.png') no-repeat 0 center;
|
||||
outline: 0;
|
||||
|
||||
/* Optional rounded corners for browsers that support it */
|
||||
-moz-border-radius: 3px;
|
||||
-khtml-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.plupload_button:hover {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.plupload_disabled, a.plupload_disabled:hover {
|
||||
color: #737373;
|
||||
border-color: #c5c5c5;
|
||||
background: #ededed url('../img/buttons-disabled.png') no-repeat 0 center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.plupload_add {
|
||||
background-position: -181px center;
|
||||
}
|
||||
|
||||
.plupload_wrapper {
|
||||
font: normal 11px Verdana,sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plupload_container {
|
||||
padding: 8px;
|
||||
background: url('../img/transp50.png');
|
||||
/*-moz-border-radius: 5px;*/
|
||||
}
|
||||
|
||||
.plupload_container input {
|
||||
border: 1px solid #DDD;
|
||||
font: normal 11px Verdana,sans-serif;
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.plupload_header {background: #2A2C2E url('../img/backgrounds.gif') repeat-x;}
|
||||
.plupload_header_content {
|
||||
background: url('../img/backgrounds.gif') no-repeat 0 -317px;
|
||||
min-height: 56px;
|
||||
padding-left: 60px;
|
||||
color: #FFF;
|
||||
}
|
||||
.plupload_header_title {
|
||||
font: normal 18px sans-serif;
|
||||
padding: 6px 0 3px;
|
||||
}
|
||||
.plupload_header_text {
|
||||
font: normal 12px sans-serif;
|
||||
}
|
||||
|
||||
.plupload_filelist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.plupload_scroll .plupload_filelist {
|
||||
height: 185px;
|
||||
background: #F5F5F5;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.plupload_filelist li {
|
||||
padding: 10px 8px;
|
||||
background: #F5F5F5 url('../img/backgrounds.gif') repeat-x 0 -156px;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.plupload_filelist_header, .plupload_filelist_footer {
|
||||
background: #DFDFDF;
|
||||
padding: 8px 8px;
|
||||
color: #42454A;
|
||||
}
|
||||
.plupload_filelist_header {
|
||||
border-top: 1px solid #EEE;
|
||||
border-bottom: 1px solid #CDCDCD;
|
||||
}
|
||||
|
||||
.plupload_filelist_footer {border-top: 1px solid #FFF; height: 22px; line-height: 20px; vertical-align: middle;}
|
||||
.plupload_file_name {float: left; overflow: hidden}
|
||||
.plupload_file_status {color: #777;}
|
||||
.plupload_file_status span {color: #42454A;}
|
||||
.plupload_file_size, .plupload_file_status, .plupload_progress {
|
||||
float: right;
|
||||
width: 80px;
|
||||
}
|
||||
.plupload_file_size, .plupload_file_status, .plupload_file_action {text-align: right;}
|
||||
|
||||
.plupload_filelist .plupload_file_name {width: 205px}
|
||||
|
||||
.plupload_file_action {
|
||||
float: right;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.plupload_file_action * {
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
li.plupload_uploading {background: #ECF3DC url('../img/backgrounds.gif') repeat-x 0 -238px;}
|
||||
li.plupload_done {color:#AAA}
|
||||
|
||||
li.plupload_delete a {
|
||||
background: url('../img/delete.gif');
|
||||
}
|
||||
|
||||
li.plupload_failed a {
|
||||
background: url('../img/error.gif');
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
li.plupload_done a {
|
||||
background: url('../img/done.gif');
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.plupload_progress, .plupload_upload_status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plupload_progress_container {
|
||||
margin-top: 3px;
|
||||
border: 1px solid #CCC;
|
||||
background: #FFF;
|
||||
padding: 1px;
|
||||
}
|
||||
.plupload_progress_bar {
|
||||
width: 0px;
|
||||
height: 7px;
|
||||
background: #CDEB8B;
|
||||
}
|
||||
|
||||
.plupload_scroll .plupload_filelist_header .plupload_file_action, .plupload_scroll .plupload_filelist_footer .plupload_file_action {
|
||||
margin-right: 17px;
|
||||
}
|
||||
|
||||
/* Floats */
|
||||
|
||||
.plupload_clear,.plupload_clearer {clear: both;}
|
||||
.plupload_clearer, .plupload_progress_bar {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
li.plupload_droptext {
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
line-height: 165px;
|
||||
}
|
|
@ -1004,3 +1004,26 @@ table.masterpalette td{
|
|||
#upcoming_plstart {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#plupload_error {
|
||||
width: 95%;
|
||||
height: 150px;
|
||||
overflow: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#plupload_error table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#plupload_error table tr {
|
||||
|
||||
}
|
||||
|
||||
#plupload_error table td {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#plupload_error table td:first-child {
|
||||
border-right: 1px solid #000000;
|
||||
}
|
|
@ -30,6 +30,15 @@ switch ($_REQUEST['act']) {
|
|||
$uiHandler->logout(TRUE);
|
||||
break;
|
||||
|
||||
case "plupload":
|
||||
$ui_tmpid = $uiHandler->pluploadFile($_REQUEST);
|
||||
if($ui_tmpid) {
|
||||
$uiHandler->SCRATCHPAD->addItem($ui_tmpid);
|
||||
}
|
||||
ob_end_clean();
|
||||
|
||||
die('{"jsonrpc" : "2.0", "error" : {}}');
|
||||
|
||||
// file/webstream handling
|
||||
case "addFileData":
|
||||
if (($ui_tmpid = $uiHandler->uploadFile(array_merge($_REQUEST, $_FILES), $ui_fmask["file"])) !== FALSE) {
|
||||
|
@ -462,16 +471,14 @@ switch ($_REQUEST['act']) {
|
|||
if ($_REQUEST['is_popup']) {
|
||||
$uiHandler->redirUrl = UI_BROWSER.'?popup[]=_reload_parent&popup[]=_close';
|
||||
}
|
||||
break;
|
||||
//break;
|
||||
}
|
||||
|
||||
|
||||
if ($uiHandler->alertMsg) {
|
||||
$_SESSION['alertMsg'] = $uiHandler->alertMsg;
|
||||
}
|
||||
//$ui_wait = 0;
|
||||
//if (ob_get_contents()) {
|
||||
// $ui_wait = 10;
|
||||
//}
|
||||
|
||||
ob_end_clean();
|
||||
if (isset($_REQUEST['target'])) {
|
||||
header('Location: ui_browser.php?act='.$_REQUEST['target']);
|
||||
|
|
|
@ -14,11 +14,77 @@
|
|||
</h1>
|
||||
|
||||
{if $editItem.type == 'audioclip' || $editItem.type == 'file'}
|
||||
|
||||
<div id="div_Data">
|
||||
{if $_REQUEST.act == 'addFileData'}
|
||||
|
||||
<form id="plupload_form">
|
||||
<div id="plupload_files"></div>
|
||||
<div id="plupload_error"><table></table></div>
|
||||
</form>
|
||||
|
||||
{literal}
|
||||
<script type="text/javascript">
|
||||
|
||||
$("#plupload_files").pluploadQueue({
|
||||
// General settings
|
||||
runtimes : 'html5',
|
||||
url : 'ui_handler.php?act=plupload',
|
||||
filters : [
|
||||
{title: "Audio Files", extensions: "ogg,mp3"}
|
||||
]
|
||||
});
|
||||
|
||||
var uploader = $("#plupload_files").pluploadQueue();
|
||||
var files_error = new Array();
|
||||
|
||||
uploader.bind('FileUploaded', function(up, file, json) {
|
||||
|
||||
if (!json.response) {
|
||||
//alert("problem");
|
||||
return;
|
||||
}
|
||||
|
||||
var j = eval("(" + json.response + ")");
|
||||
|
||||
if(j.error.message) {
|
||||
|
||||
var row = $("<tr/>")
|
||||
.append('<td>' + file.name +'</td>')
|
||||
.append('<td>' + j.error.message + '</td>');
|
||||
|
||||
$("#plupload_error").find("table").append(row);
|
||||
files_error.push(file);
|
||||
|
||||
if(files_error.length % 2 === 0){
|
||||
row.addClass("blue1");
|
||||
}
|
||||
else {
|
||||
row.addClass("blue2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(up.state === plupload.STOPPED){
|
||||
var i;
|
||||
for( i=0; i< files_error.length; i++ ){
|
||||
up.removeFile(files_error[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uploader.bind('Error', function(up, err) {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
</script>
|
||||
{/literal}
|
||||
|
||||
{*
|
||||
{UIBROWSER->fileForm id=$editItem.id folderId=$editItem.folderId assign="dynform"}
|
||||
{include file="sub/dynForm_plain.tpl}
|
||||
{assign var="_uploadform" value=null}
|
||||
*}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -27,7 +93,10 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
{if $editItem.type == 'webstream'}
|
||||
|
||||
<div id="div_Data">
|
||||
{UIBROWSER->webstreamForm id=$editItem.id folderId=$editItem.folderId assign="dynform"}
|
||||
{include file="sub/dynForm_plain.tpl}
|
||||
|
@ -43,6 +112,7 @@
|
|||
{include file="file/metadataform.tpl"}
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -9,10 +9,17 @@
|
|||
{* <link rel="stylesheet" href="styles.css"> *}
|
||||
<link href="styles_campcaster.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<link href="assets/plupload/plupload.queue.css" rel="stylesheet" type="text/css" />
|
||||
<script type="text/javascript" src="assets/jquery-1.3.2.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="assets/plupload/plupload.full.min.js"></script>
|
||||
<script type="text/javascript" src="assets/plupload/jquery.plupload.queue.min.js"></script>
|
||||
|
||||
{include file="script/basics.js.tpl"}
|
||||
{include file="script/contextmenu.js.tpl"}
|
||||
{include file="script/collector.js.tpl"}
|
||||
{include file="script/alttext.js.tpl"}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -105,6 +105,158 @@ class uiHandler extends uiBase {
|
|||
}
|
||||
} // fn logout
|
||||
|
||||
function processFile($audio_file, $caller){
|
||||
|
||||
global $CC_CONFIG;
|
||||
|
||||
if ($this->testForAudioType($audio_file) === FALSE) {
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "uses an unsupported file type."}}');
|
||||
}
|
||||
|
||||
$md5 = md5_file($audio_file);
|
||||
$duplicate = StoredFile::RecallByMd5($md5);
|
||||
if ($duplicate) {
|
||||
$_SESSION['plupload'] = "is duplicate";
|
||||
if (PEAR::isError($duplicate)) {
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": ' . $duplicate->getMessage() .'}}');
|
||||
}
|
||||
else {
|
||||
$duplicateName = $this->gb->getMetadataValue($duplicate->getId(), UI_MDATA_KEY_TITLE, $this->sessid);
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "An identical audioclip named ' . $duplicateName . ' already exists in the storage server."}}');
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = camp_get_audio_metadata($audio_file);
|
||||
|
||||
if (PEAR::isError($metadata)) {
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": ' + $metadata->getMessage() + '}}');
|
||||
}
|
||||
|
||||
// #2196 no id tag -> use the original filename
|
||||
if (basename($audio_file) == $metadata['dc:title']) {
|
||||
$metadata['dc:title'] = basename($audio_file);
|
||||
$metadata['ls:filename'] = basename($audio_file);
|
||||
}
|
||||
|
||||
// bsSetMetadataBatch doesnt like these values
|
||||
unset($metadata['audio']);
|
||||
unset($metadata['playtime_seconds']);
|
||||
|
||||
$values = array(
|
||||
"filename" => basename($audio_file),
|
||||
"filepath" => $audio_file,
|
||||
"filetype" => "audioclip",
|
||||
"mime" => $metadata["dc:format"],
|
||||
"md5" => $md5
|
||||
);
|
||||
$storedFile = $this->gb->putFile(NULL, $values, $this->sessid);
|
||||
|
||||
if (PEAR::isError($storedFile)) {
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": ' + $storedFile->getMessage() + '}}');
|
||||
}
|
||||
|
||||
$result = $this->gb->bsSetMetadataBatch($storedFile->getId(), $metadata);
|
||||
|
||||
return $storedFile->getId();
|
||||
}
|
||||
|
||||
function pluploadFile($data)
|
||||
{
|
||||
header('Content-type: text/plain; charset=UTF-8');
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
|
||||
// Settings
|
||||
$targetDir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload";
|
||||
$cleanupTargetDir = false; // Remove old files
|
||||
$maxFileAge = 60 * 60; // Temp file age in seconds
|
||||
|
||||
// 5 minutes execution time
|
||||
@set_time_limit(5 * 60);
|
||||
|
||||
// Get parameters
|
||||
$chunk = isset($_REQUEST["chunk"]) ? $_REQUEST["chunk"] : 0;
|
||||
$chunks = isset($_REQUEST["chunks"]) ? $_REQUEST["chunks"] : 0;
|
||||
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
|
||||
|
||||
// Clean the fileName for security reasons
|
||||
//$fileName = preg_replace('/[^\w\._]+/', '', $fileName);
|
||||
|
||||
// Create target dir
|
||||
if (!file_exists($targetDir)) {
|
||||
@mkdir($targetDir);
|
||||
}
|
||||
|
||||
// Remove old temp files
|
||||
if (is_dir($targetDir) && ($dir = opendir($targetDir))) {
|
||||
while (($file = readdir($dir)) !== false) {
|
||||
$filePath = $targetDir . DIRECTORY_SEPARATOR . $file;
|
||||
|
||||
// Remove temp files if they are older than the max age
|
||||
if (preg_match('/\\.tmp$/', $file) && (filemtime($filePath) < time() - $maxFileAge))
|
||||
@unlink($filePath);
|
||||
}
|
||||
|
||||
closedir($dir);
|
||||
}
|
||||
else {
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}');
|
||||
}
|
||||
|
||||
// Look for the content type header
|
||||
if (isset($_SERVER["HTTP_CONTENT_TYPE"]))
|
||||
$contentType = $_SERVER["HTTP_CONTENT_TYPE"];
|
||||
|
||||
if (isset($_SERVER["CONTENT_TYPE"]))
|
||||
$contentType = $_SERVER["CONTENT_TYPE"];
|
||||
|
||||
if (strpos($contentType, "multipart") !== false) {
|
||||
if (isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
|
||||
// Open temp file
|
||||
$out = fopen($targetDir . DIRECTORY_SEPARATOR . $fileName, $chunk == 0 ? "wb" : "ab");
|
||||
if ($out) {
|
||||
// Read binary input stream and append it to temp file
|
||||
$in = fopen($_FILES['file']['tmp_name'], "rb");
|
||||
|
||||
if ($in) {
|
||||
while ($buff = fread($in, 4096))
|
||||
fwrite($out, $buff);
|
||||
} else
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
|
||||
|
||||
fclose($out);
|
||||
unlink($_FILES['file']['tmp_name']);
|
||||
} else
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
|
||||
} else
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
|
||||
} else {
|
||||
// Open temp file
|
||||
$out = fopen($targetDir . DIRECTORY_SEPARATOR . $fileName, $chunk == 0 ? "wb" : "ab");
|
||||
if ($out) {
|
||||
// Read binary input stream and append it to temp file
|
||||
$in = fopen("php://input", "rb");
|
||||
|
||||
if ($in) {
|
||||
while ($buff = fread($in, 4096))
|
||||
fwrite($out, $buff);
|
||||
} else
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
|
||||
|
||||
fclose($out);
|
||||
return $this->processFile($targetDir . DIRECTORY_SEPARATOR . $fileName);
|
||||
|
||||
} else
|
||||
die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
|
||||
}
|
||||
|
||||
// Return JSON-RPC response
|
||||
die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');
|
||||
}
|
||||
|
||||
|
||||
// --- files ---
|
||||
/**
|
||||
|
@ -127,8 +279,11 @@ class uiHandler extends uiBase {
|
|||
$id = $formdata['id'];
|
||||
$folderId = $formdata['folderId'];
|
||||
|
||||
|
||||
|
||||
if (Greenbox::getFileType($folderId) != 'Folder') {
|
||||
$this->_retMsg('The target is not a folder.');
|
||||
$this->_retMsg('The target is not a folder: ' . $folderId . ' id: ' . $id);
|
||||
|
||||
$this->redirUrl = UI_BROWSER."?act=fileList";
|
||||
return FALSE;
|
||||
}
|
||||
|
@ -229,21 +384,21 @@ class uiHandler extends uiBase {
|
|||
// This is really confusing: the import script does not do it
|
||||
// this way. Which way is the right way?
|
||||
$this->setMetadataValue($id, UI_MDATA_KEY_DURATION, Playlist::secondsToPlaylistTime($ia['playtime_seconds']));
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_FORMAT, UI_MDATA_VALUE_FORMAT_FILE);
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_FORMAT, UI_MDATA_VALUE_FORMAT_FILE);
|
||||
|
||||
// some data from raw audio
|
||||
// if (isset($ia['audio']['channels'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_CHANNELS, $ia['audio']['channels']);
|
||||
// }
|
||||
// if (isset($ia['audio']['sample_rate'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_SAMPLERATE, $ia['audio']['sample_rate']);
|
||||
// }
|
||||
// if (isset($ia['audio']['bitrate'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_BITRATE, $ia['audio']['bitrate']);
|
||||
// }
|
||||
// if (isset($ia['audio']['codec'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_ENCODER, $ia['audio']['codec']);
|
||||
// }
|
||||
// if (isset($ia['audio']['channels'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_CHANNELS, $ia['audio']['channels']);
|
||||
// }
|
||||
// if (isset($ia['audio']['sample_rate'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_SAMPLERATE, $ia['audio']['sample_rate']);
|
||||
// }
|
||||
// if (isset($ia['audio']['bitrate'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_BITRATE, $ia['audio']['bitrate']);
|
||||
// }
|
||||
// if (isset($ia['audio']['codec'])) {
|
||||
// $this->setMetadataValue($id, UI_MDATA_KEY_ENCODER, $ia['audio']['codec']);
|
||||
// }
|
||||
|
||||
// from id3 Tags
|
||||
// loop main, music, talk
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue