LLWiki正在建设中,欢迎加入我们!
MediaWiki:Gadget-uploader-rewrite.js
跳转到导航
跳转到搜索
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
//<nowiki>
// 引自[[moegirl:user:東東君/js/uploader.js|moegirl:user:-{東東君}-/js/uploader.js]]
"use strict";
var api = new mw.Api();
/**
* @param {object} options
* @param {File} options.body
* @param {string} options.fileName
* @param {string} options.comment
* @param {string} options.pageContent
*/
function upload({ body, fileName, comment, pageContent }) {
return new Promise(function (resolve, reject) {
var data = {
filename: fileName,
comment: comment,
text: pageContent,
format: 'json',
ignorewarnings: 1
};
if (typeof body == 'string') {
data.url = body;
data.action = 'upload';
console.log('Upload by url: ' + body);
api.postWithToken('csrf', data).then(resolve, reject);
} else {
api.upload(body, data).then(resolve, reject);
}
});
}
/**
* @param {string[]} fileNames
*/
function checkFileNames(fileNames) {
return api.get({
action: 'query',
titles: fileNames.map(item => 'File:' + item).join('|'),
converttitles: 1,
formatversion: 2
}).then(function (data) {
var result = {};
data.query.pages.forEach( function(item) {
result[item.title.substring(5)] = !('missing' in item);
return result;
} );
return result;
});
}
$.when( $.ready, mw.loader.getScript( 'https://unpkg.com/[email protected]/dist/vue.min.js' ) ).then(function() {
$(document.body).append('<div id="widget-fileUploader" style="display:none">');
// 向“更多”菜单注入按钮
$('#p-cactions ul').append('<li id="btn-fileUploader"><a title="上传文件">' + wgULS("批量上传文件", "批次上傳檔案") + '</a></li>');
$('#btn-fileUploader').click(() => {
$('#widget-fileUploader').fadeIn(200);
$('#content').css('position', 'static');
});
const template = `
<div id="widget-fileUploader" class="uploader-container">
<input ref="fileInput" style="display:none" type="file" multiple="multiple" :accept="allowedFileTypes.map(item => '.' + item).join(',')" @change="addFileByFileSelector" />
<div class="uploader-closeBtn" @click="hideWidget">×</div>
<div class="uploader-body">
<div class="uploader-fileList" @dragenter.prevent="() => {}" @dragover.prevent="() => {}" @drop.prevent="addFileByDropping">
<div v-if="files.length === 0" key="hintMask" class="hintMask" @click="$refs.fileInput.click()">
<div class="hintText">` + wgULS("点此添加文件,或将文件拖拽至此", "點此添加檔案,或將檔案拖拽至此") + `</div>
</div>
<div v-for="(item, index) in files" :key="item.body.lastModified" class="item" :data-name="item.fileName" :data-selected="index === focusedFileIndex" title="单击选中文件,双击复制文件名" @click="focusFile(index)">
<img v-if="isImageFile(item.body)" :src="item.objectUrl" />
<div v-else class="unablePreviewHint">
<div>` + wgULS("不支持预览的文件类型", "不支援預覽的檔案類型") + `</div>
<div v-if="typeof item.body !== 'string'" class="type">Mimetype: {{ item.body.type }}</div>
</div>
<div class="removeBtn" @click.stop="files.splice(index, 1)">×</div>
</div>
<div v-if="files.length !== 0" class="item addFileBox" @click="$refs.fileInput.click()" />
</div>
<div class="uploader-panel">
<div class="block">
<div class="input-container" title="上传后使用文件时的名字,要求不能和现有文件重复">
<span>` + wgULS("文件", "檔案") + `名:</span>
<input v-model.trim="form.fileName" />
</div>
<div class="input-container categoryInput" title="所有文件共享分类">
<span>` + wgULS("分 类", "分 類") + `:</span>
<input ref="categoryInput" v-model.trim="form.categoryInput" @input="loadCategoryHint" @keydown.enter="addCategory(form.categoryInput)" @keydown.up.prevent="handlerFor_categoryInput_wasKeyDowned" />
<div class="inputHint">` + wgULS("按下回车键添加分类", "按下確認鍵添加分類") + `</div>
<div ref="categoryHints" v-if="categoryHints.length !== 0" class="categoryHints" @keydown.enter="addCategory(categoryHints[categoryHintFocusedIndex])" @keydown.prevent="handlerFor_categoryHints_wasKeyDowned">
<div v-for="(item, index) in categoryHints" class="item" :data-selected="index === categoryHintFocusedIndex" @click="addCategory(item)">{{ item }}</div>
</div>
</div>
<div class="categories">
<div v-for="(item, index) in form.categories" class="item" title="点击删除分类" @click="form.categories.splice(index, 1)" >{{ item }}</div>
</div>
</div>
<div class="block">
<div class="input-container">
<span>角色名:</span>
<input v-model.trim="form.charaName" />
</div>
<div class="input-container">
<span>` + wgULS("源地址", "原位址") + `:</span>
<input v-model.trim="form.source" />
</div>
<div class="input-container" style="visibility:hidden">
<span>作 者:</span>
<input v-model.trim="form.author" />
</div>
</div>
<div class="block" style="flex-direction:column; justify-content:space-around; align-items:flex-start;">
<div class="input-container" title="所有文件共享前缀">
<span>` + wgULS("添加前缀", "添加前綴") + `:</span>
<input v-model.trim="form.prefix" style="width:calc(100% - 6em)" />
</div>
<div class="input-container" style="justify-content:flex-start;">
<select v-model.trim="form.license">
<option disabled="disabled" value="">` + wgULS("选择授权协议(将鼠标放在选项上显示详情)", "選擇授權協定(將滑鼠放在選項上顯示詳情)") + `</option>
<optgroup label="CC` + wgULS("协议", "協定") + `">
<option value="CC Zero" title="作者授权以无著作权方式使用">CC-0</option>
<option value="CC BY" title="作者授权以署名方式使用,该授权需兼容4.0协议">CC BY 4.0</option>
<option value="CC BY-SA" title="作者授权以署名-相同方式方式使用,该授权需兼容4.0协议">CC BY-SA 4.0</option>
<option value="CC BY-NC-SA" title="作者授权以署名-非商业使用-相同协议方式使用,该授权需兼容4.0协议">CC BY-NC-SA 4.0</option>
</optgroup>
<optgroup label="` + wgULS("公有领域", "公有領域") + `">'
<option value="PD-Old">` + wgULS("作者离世一定年限后流入公有领域", "作者離世一定年限後流入公有領域") + `</option>
<option value="PD-Other">` + wgULS("其他原因流入公有领域", "其他原因流入公有領域") + `</option>
</optgroup>
<optgroup label="其他">
<option value="Copyright" title="原作者没有明确的授权声明">` + wgULS("原作者保留权利", "原作者保留權利") + `</option>
<option value="none:gotoCommons">` + wgULS("原作者授权LLWiki使用", "原作者授權LLWiki使用") + `</option>
<option value="可自由使用" title="作者放弃版权或声明可自由使用">可自由使用</option>
<option value="LLWiki版权所有">LLWiki` + wgULS("版权所有", "版權所有") + `</option>
</optgroup>
</select>
</div>
<div class="buttons">
<button @click="addSourceUrlFile">` + wgULS("添加源地址上传", "添加原始地址上傳") + `</button>
<button :disabled="status === 2" title="执行上传文件" @click="submit(false)">` + wgULS("上传", "上傳") + `</button>
<button :disabled="status === 2" title="在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传" @click="submit(true)">` + wgULS("差分上传", "差分上傳") + `</button>
<button title="将当前文件除文件名的信息同步到全部文件" @click="asyncCurrentFileInfo">同步` + wgULS("文件", "檔案") + `信息</button>
<button @click="showManual">` + wgULS("使用说明", "使用說明") + `</button>
</div>
</div>
</div>
</div>
</div>
`;
new Vue({
el: '#widget-fileUploader',
template,
data() {
return {
allowedFileTypes: ['ogg', 'mp3', 'png', 'gif', 'jpg', 'jpeg', 'webp'],
files: [], // 待上传的文件
focusedFileIndex: 0,
categoryHints: [],
categoryInputDebounceTimeoutKey: 0,
categoryHintFocusedIndex: -1,
status: 1, // 0:失败,1:初始化,2:提交中,3:成功
form: {
fileName: '',
categoryInput: '', // 分类输入栏
categories: [], // 实际要提交的分类
charaName: '',
author: '',
source: '',
prefix: '',
license: ''
},
doubleClickTimeoutKey: 0 // 用于双击复制文件名
};
},
mounted() {
$('#widget-fileUploader').hide();
},
watch: {
files() {
this.focusedFileIndex === 0 && this.focusFile(0);
},
form: {
deep: true,
handler() {
if (!this.files[this.focusedFileIndex]) { return; }
this.files[this.focusedFileIndex] = {
...this.files[this.focusedFileIndex],
fileName: this.form.fileName,
author: this.form.author,
charaName: this.form.charaName,
source: this.form.source,
license: this.form.license
};
}
},
license(val) {
if (val === 'none:gotoCommons') {
alert(wgULS('该协议需要手动填写授权证明,请到特殊页面上传', '該協定需要手動填寫授權證明,請到特殊頁面上傳'));
window.open('https://llwiki.org/zh/Special:上传文件', '_blank');
}
}
},
computed: {
license() {
return this.form.license;
},
},
methods: {
createFileItem(fileBody) {
return {
body: fileBody,
objectUrl: typeof fileBody === 'string' ? fileBody : URL.createObjectURL(fileBody),
fileName: typeof fileBody === 'string' ? fileBody.replace(/.+\/(.+?)$/, '$1') : fileBody.name,
author: '',
charaName: '',
source: '',
license: 'Copyright'
};
},
isImageFile(fileBody) {
const imageType = ['jpg', 'png', 'jpeg', 'gif', 'webp'];
return imageType.includes( (typeof fileBody === 'string' ? fileBody : fileBody.name).replace(/.+\.(.+?)$/, '$1').toLowerCase() );
},
hideWidget() {
$('#widget-fileUploader').fadeOut(200);
$('#content').css('position', 'relative');
},
loadCategoryHint() {
clearTimeout(this.categoryInputDebounceTimeoutKey);
this.categoryInputDebounceTimeoutKey = setTimeout(() => {
if (this.form.categoryInput === '') { return; }
api.get({action: "query", list: "search", srwhat: 'title', srsearch: this.form.categoryInput, srnamespace: "14", srlimit: "20", srprop: "", formatversion: 2})
.then(data => {
const hints = data.query.search.map(item => item.title.substring(9));
this.categoryHints = hints;
});
}, 500);
},
resetCategory() {
this.form.categoryInput = '';
this.categoryHints = [];
this.categoryHintFocusedIndex = -1;
},
addCategory(categoryName) {
this.form.categories.push(categoryName);
this.resetCategory();
},
// 实现上下键切换分类提示
handlerFor_categoryHints_wasKeyDowned(e) {
if (e.code === 'ArrowUp') {
this.categoryHintFocusedIndex++;
if (this.categoryHintFocusedIndex > this.categoryHints.length - 1) {
this.categoryHintFocusedIndex = 0;
}
}
if (e.code === 'ArrowDown') {
this.categoryHintFocusedIndex--;
if (this.categoryHintFocusedIndex < 0) {
this.$refs.categoryInput.focus();
}
}
this.categoryHintFocusedIndex >= 0 && this.$refs.categoryHints.querySelectorAll('div')[this.categoryHintFocusedIndex].scrollIntoView();
},
handlerFor_categoryInput_wasKeyDowned() {
if (this.categoryHints.length === 0 || !this.$refs.categoryHints) { return; }
this.$refs.categoryHints.focus();
this.categoryHintFocusedIndex = 0;
},
addFileByFileSelector(e) {
const originalFileList = e.target.files;
[].forEach.call(originalFileList, file => {
if (this.files.length === 50) { return; }
if (file.size / 1024 / 1024 > 20) return alert(wgULS("文件", "檔案") + `【${file.name}】` + wgULS("大小超过20MB,无法上传", "大小超過20MB,無法上傳") + `!`);
this.files.push(this.createFileItem(file));
});
e.target.value = '';
if (this.files.length === 50) mw.notify(wgULS('一次最多上传50个文件', '一次最多上傳50個檔案'), { type: 'wran' });
},
addFileByDropping(e) {
const originalFileList = e.dataTransfer.files;
[].forEach.call(originalFileList, file => {
if (this.files.length === 50) { return; }
if (!this.allowedFileTypes.includes( file.name.replace(/.+\.(.+?)$/, '$1').toLowerCase() )) return alert(`【${file.name}】` + wgULS("不支持上传这种格式的文件", "不支持上傳這種格式的檔案") + `!`);
if (file.size / 1024 / 1024 > 20) return alert(`【${file.name}】的` + wgULS("大小超过20MB,无法上传", "大小超過20MB,無法上傳") + `!`);
this.files.push(this.createFileItem(file));
});
if (this.files.length === 50) mw.notify(wgULS('一次最多上传50个文件', '一次最多上傳50個檔案'), { type: 'wran' });
},
focusFile(index) {
this.focusedFileIndex = index;
const file = this.files[index];
this.form = {
...this.form,
fileName: file.fileName,
author: file.author,
charaName: file.charaName,
source: file.source,
license: file.license
};
// 实现双击复制文件名
if (this.doubleClickTimeoutKey === 0) {
this.doubleClickTimeoutKey = setTimeout(() => {
this.doubleClickTimeoutKey = 0;
}, 300);
} else {
mw.notify('已复制' + wgULS("文件", "檔案") + '名');
this.copyFileName(this.form.prefix + file.fileName);
clearTimeout(this.doubleClickTimeoutKey);
this.doubleClickTimeoutKey = 0;
}
},
addSourceUrlFile() {
var url = (prompt(wgULS('请输入文件地址', '請輸入檔案位址') + ':') || '').trim();
if (!url) { return; }
this.files.push(this.createFileItem(url));
},
copyFileName(fileName) {
const inputTag = document.createElement('input');
inputTag.value = fileName;
inputTag.style.cssText = `
position: fixed;
left: -9999px;
`;
document.body.appendChild(inputTag);
inputTag.focus();
document.execCommand('selectAll');
document.execCommand('copy');
setTimeout(() => document.body.removeChild(inputTag), 1000);
},
asyncCurrentFileInfo() {
if (!confirm(wgULS('确定要将当前选中的文件信息(不含文件名)同步到所有文件中?', '確定要將當前選中的檔案資訊(不含檔案名)同步到所有檔案中?'))) { return; }
const currentFile = this.files[this.focusedFileIndex];
if (!currentFile) return mw.notify(wgULS('当前未选中文件', '當前未選中檔案'));
this.files.forEach(item => {
item.author = currentFile.author;
item.charaName = currentFile.charaName;
item.source = currentFile.source;
item.license = currentFile.license;
});
mw.notify('已同步');
},
showManual() {
alert(wgULS([
'使用说明',
'1. 该插件支持拖拽上传、批量上传。',
'2. 若文件上传时发生异常,请以监视列表为准。',
'3. 每个文件拥有独立的信息,但“分类”和“添加前缀”是共享的。在需要同步每个文件的角色名等信息时可以使用“同步文件信息”的功能。',
'4. 什么是“差分上传”:在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传。',
'5. 双击文件可以自动复制“前缀 + 文件名”。'
].join('\n'), [
'使用說明',
'1. 該外掛程式支援拖拽上傳、批次上傳。',
'2. 若檔案上傳時發生異常,請以監視清單為準。',
'3. 每個檔案擁有獨立的資訊,但「分類」和「添加前綴」是共享的。在需要同步每個檔案的角色名等資訊時可以使用「同步檔案資訊」的功能。',
'4. 什麼是「差分上傳」:在發生檔案名已存在的情況時,自動濾掉已存在的檔案。通常用於在上一次批次上傳中一部分失敗後,再次嘗試將之前沒傳上去的檔案重新上傳。',
'5. 雙擊檔案可以自動複製「前綴 + 檔案名」。'
].join('\n')));
},
async submit(diffMode) {
if (this.files.length === 0) return mw.notify(wgULS('您还没有上传任何文件', '您還沒有上傳任何檔案'), { type: 'warn' });
if (this.files.some(item => item.fileName === '')) return mw.notify(wgULS('存在文件名为空的文件', '存在檔案名為空的檔案'), { type: 'warn' });
const duplicateFiles = this.files.reduce((result, item) => {
const isDuplicate = this.files.filter(item2 => item2.fileName === item.fileName).length > 1;
isDuplicate && result.push(item);
return result;
}, []);
if (duplicateFiles.length > 0) return alert([
wgULS('这些文件名发生了重复,请不要给要上传的文件设置相同的名称:', '這些檔案名發生了重複,請不要給要上傳的檔案設定相同的名稱:'),
...duplicateFiles.map(item => item.fileName)
].join('\n'));
const authorizedFiles = this.files.filter(item => item.license === 'none:gotoCommons');
if (authorizedFiles.length > 0) return alert([
wgULS('这些文件的授权协议不允许使用上传工具,请在本次上传中删除,并前往特殊页面填写授权信息后上传:', '這些檔案的授權協定不允許使用上傳工具,請在本次上傳中刪除,並前往特殊頁面填寫授權資訊後上傳:'),
...authorizedFiles.map(item => item.fileName),
].join('\n'));
if (!confirm(wgULS('确定要开始上传吗?', '確定要開始上傳嗎?'))) { return; }
let postData = this.files.map(item => {
const metaCategories = (item.charaName ? `[[Category:${item.charaName}${wgULS('图片', '圖片')}]]` : '');
const source = item.source ? `源地址:${item.source}` : '';
const comment = metaCategories + source;
const pageContent = [
'== 摘要 ==',
metaCategories + this.form.categories.map(item => `[[Category:${item}]]`).join(''),
source,
'== 许可协议 ==',
`{{${item.license}}}`
].join('\n');
return {
body: item.body,
fileName: this.form.prefix + item.fileName,
comment,
pageContent
};
});
mw.notify(wgULS("开始", "開始") + `${diffMode ? '差分' : ''}` + wgULS("上传", "上傳") + `,共${postData.length}` + wgULS("个文件", "個檔案") + `...`);
console.log(`---- FileUploader 开始${diffMode ? '差分' : ''}上传,共${postData.length}个文件 ----`);
this.status = 2;
const printLogFn = (type = 'info') => msg => { mw.notify(msg, { type }); console.log(msg) };
const printLog = printLogFn();
printLog.warn = printLogFn('warn');
printLog.error = printLogFn('error');
try {
const checkedResult = await checkFileNames(postData.map(item => item.fileName));
const existedFiles = postData.filter(item => checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]); // 首字母转大写,因为checkedResult返回的文件名首字母是大写
if (existedFiles.length > 0 && !diffMode) {
alert([
wgULS('这些文件名已被使用,请为对应的文件更换其他名称:', '這些檔案名已被使用,請為對應的檔案更換其他名稱'),
...existedFiles.map(item => item.fileName)
].join('\n'));
this.status = 1;
return;
}
if (diffMode) postData = postData.filter(item => !checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]);
if (diffMode && postData.length === 0) {
alert(wgULS('差分模式下没有可以上传的文件', '差分模式下沒有可以上傳的檔案'));
this.status = 1;
return;
}
printLog.warn("差分" + wgULS("上传共需要上传", "上傳共需要上傳") + `${postData.length}` + wgULS("个文件", "個檔案"));
let uploadResults = [];
if (postData.length <= 3) {
uploadResults = await Promise.all(postData.map(item =>
new Promise(resolve => {
upload(item)
.then(() => {
printLog(`【${item.fileName}】` + wgULS("上传成功", "上傳成功"));
resolve({ fileName: item.fileName, result: true });
})
.catch(() => {
printLog.error(`【${item.fileName}】` + wgULS("上传失败", "上傳失敗"));
resolve({ fileName: item.fileName, result: false });
});
}))
);
} else {
alert(wgULS('上传的文件超过三个,执行分段上传,请耐心等待。进入控制台可查看全部日志(按F12后选择Console)。', '上傳的檔案超過三個,執行分段上傳,請耐心等待。進入控制臺可檢視全部日誌(按F12後選擇Console)。'));
printLog.warn(wgULS('上传文件超过3个,执行分段上传', '上傳檔案超過3個,執行分段上傳'));
// 分段上传
const segmentedPostData = postData.reduce((result, item) => {
if (result.length === 0) result.push([]);
if (result[result.length - 1].length === 3) result.push([]);
result[result.length - 1].push(item);
return result;
}, []);
console.log(segmentedPostData);
for (let i=0, len=segmentedPostData.length; i < len; i++) {
printLog(`共${len}个分段,现在开始第${i + 1}个`);
const segment = segmentedPostData[i];
const segmentedUploadResult = await Promise.all(segment.map(item =>
new Promise(resolve => {
upload(item)
.then(() => {
printLog(`【${item.fileName}】上传成功`);
resolve({ fileName: item.fileName, result: true });
})
.catch(() => {
printLog.error(`【${item.fileName}】上传失败`);
resolve({ fileName: item.fileName, result: false });
});
}))
);
uploadResults.push(...segmentedUploadResult);
printLog(`第${i + 1}个分段完成,其中${segmentedUploadResult.filter(item => item.result).length}个成功,${segmentedUploadResult.filter(item => !item.result).length}个失败`);
}
}
const report = wgULS([
`全部上传结果:共计${uploadResults.length}个文件,其中${uploadResults.filter(item => item.result).length}个成功,${uploadResults.filter(item => !item.result).length}个失败`,
...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}】${item.result ? '成功' : '失败'}`)
].join('\n'), [
`全部上傳結果:共計${uploadResults.length}個檔案,其中${uploadResults.filter(item => item.result).length}個成功,${uploadResults.filter(item => !item.result).length}個失敗`,
...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}】${item.result ? '成功' : '失敗'}`)
].join('\n'));
console.log(report);
alert(report);
this.status = 3;
} catch (e) {
console.log('上传流程出现错误', e);
mw.notify(wgULS('网络错误,请重试', '網路錯誤,請重試'), { type: 'error' });
this.status = 0;
};
}
}
});
});
//</nowiki>