MediaWiki:Gadget-Gallery.js:修订间差异
外观
LocalAdmin(留言 | 贡献) Create Instagram-style gallery gadget |
LocalAdmin(留言 | 贡献) Auto-rename on duplicate filename instead of overwriting |
||
| 第33行: | 第33行: | ||
} | } | ||
/* ── Build one grid item | /* ── Build one grid item ────────────────────────────────────────*/ | ||
function makeItem( page, cls ) { | function makeItem( page, cls ) { | ||
var info = page.imageinfo[ 0 ]; | var info = page.imageinfo[ 0 ]; | ||
var thumb = info.thumburl || info.url; | var thumb = info.thumburl || info.url; | ||
var title = page.title; | var title = page.title; | ||
var href = mw.util.getUrl( title ); | var href = mw.util.getUrl( title ); | ||
var a | var a = document.createElement( 'a' ); | ||
a.href | a.href = href; | ||
a.className = cls + ' image'; | a.className = cls + ' image'; | ||
a.title = title.replace( /^(File|文件):/, '' ); | a.title = title.replace( /^(File|文件):/, '' ); | ||
var img | var img = document.createElement( 'img' ); | ||
img.src | img.src = thumb; | ||
img.alt | img.alt = a.title; | ||
img.loading = 'lazy'; | img.loading = 'lazy'; | ||
a.appendChild( img ); | a.appendChild( img ); | ||
return a; | return a; | ||
} | |||
/* ── Upload a single file, auto-renaming on conflict ──────────── | |||
* Strategy: try original name → on exists/duplicate warning, | |||
* insert "_N" before the extension and retry (up to 99 times). | |||
* ─────────────────────────────────────────────────────────────── */ | |||
function uploadOne( file, token, attempt ) { | |||
attempt = attempt || 0; | |||
/* Build filename with optional suffix */ | |||
var filename; | |||
if ( attempt === 0 ) { | |||
filename = file.name; | |||
} else { | |||
var m = file.name.match( /^(.*?)(\.[^.]+)?$/ ); | |||
var base = m[ 1 ] || file.name; | |||
var ext = m[ 2 ] || ''; | |||
filename = base + '_' + attempt + ext; | |||
} | |||
if ( attempt > 99 ) { | |||
return Promise.resolve( { ok: false, filename: filename, error: '重命名次数超限' } ); | |||
} | |||
var fd = new FormData(); | |||
fd.append( 'action', 'upload' ); | |||
fd.append( 'format', 'json' ); | |||
fd.append( 'filename', filename ); | |||
fd.append( 'file', file ); | |||
fd.append( 'token', token ); | |||
/* No ignorewarnings — let MediaWiki tell us about conflicts */ | |||
fd.append( 'text', '[[Category:图库]]\n上传自图库。' ); | |||
fd.append( 'comment', '图库上传' ); | |||
return fetch( mw.config.get( 'wgScriptPath' ) + '/api.php', { | |||
method: 'POST', body: fd, credentials: 'same-origin' | |||
} ) | |||
.then( function ( r ) { return r.json(); } ) | |||
.then( function ( res ) { | |||
/* Success */ | |||
if ( res.upload && res.upload.result === 'Success' ) { | |||
return { ok: true, filename: filename }; | |||
} | |||
/* Conflict warnings → retry with next suffix */ | |||
var w = res.upload && res.upload.warnings; | |||
if ( w && ( w.exists || w[ 'page-exists' ] || w.duplicate || w.badfilename ) ) { | |||
return uploadOne( file, token, attempt + 1 ); | |||
} | |||
/* Any other error */ | |||
var msg = ( res.error && res.error.info ) || | |||
( w && JSON.stringify( w ) ) || '未知错误'; | |||
return { ok: false, filename: filename, error: msg }; | |||
} ) | |||
.catch( function () { | |||
return { ok: false, filename: filename, error: '网络错误' }; | |||
} ); | |||
} | } | ||
| 第64行: | 第122行: | ||
var canUpload = groups.indexOf( 'user' ) !== -1 || groups.indexOf( 'sysop' ) !== -1; | var canUpload = groups.indexOf( 'user' ) !== -1 || groups.indexOf( 'sysop' ) !== -1; | ||
root.innerHTML = | |||
( canUpload | |||
? '<div id="gal-drop">' + | |||
'<div id="gal-drop">' + | ' <div class="gal-drop-inner">' + | ||
' <span class="gal-drop-icon">📷</span>' + | |||
' <p>将图片拖放至此上传</p>' + | |||
' <p class="gal-drop-sub">或 <label class="gal-browse">点击选择文件' + | |||
' <input type="file" id="gal-file-input" multiple accept="image/*">' + | |||
' </label></p>' + | |||
' </div>' + | |||
' <div id="gal-status"></div>' + | |||
'</div>' | |||
: '' ) + | |||
' | '<div id="gal-grid" class="gal-grid"></div>'; | ||
var grid = document.getElementById( 'gal-grid' ); | var grid = document.getElementById( 'gal-grid' ); | ||
| 第89行: | 第144行: | ||
fetchImages( 200, function ( pages ) { | fetchImages( 200, function ( pages ) { | ||
grid.innerHTML = ''; | grid.innerHTML = ''; | ||
if ( pages.length | if ( !pages.length ) { | ||
grid.innerHTML = '<div class="gal-empty">还没有图片,快来上传第一张吧!</div>'; | grid.innerHTML = '<div class="gal-empty">还没有图片,快来上传第一张吧!</div>'; | ||
return; | return; | ||
} | } | ||
pages.forEach( function ( p ) { | pages.forEach( function ( p ) { grid.appendChild( makeItem( p, 'gal-item' ) ); } ); | ||
} ); | } ); | ||
} | } | ||
| 第102行: | 第155行: | ||
if ( !canUpload ) { return; } | if ( !canUpload ) { return; } | ||
var drop = document.getElementById( 'gal-drop' ); | var drop = document.getElementById( 'gal-drop' ); | ||
var input = document.getElementById( 'gal-file-input' ); | var input = document.getElementById( 'gal-file-input' ); | ||
drop.addEventListener( 'dragover', function ( e ) { | drop.addEventListener( 'dragover', function ( e ) { e.preventDefault(); drop.classList.add( 'gal-dragover' ); } ); | ||
drop.addEventListener( 'dragleave', function () { drop.classList.remove( 'gal-dragover' ); } ); | |||
drop.addEventListener( 'drop', function ( e ) { e.preventDefault(); drop.classList.remove( 'gal-dragover' ); uploadFiles( e.dataTransfer.files ); } ); | |||
drop.addEventListener( 'dragleave', function () { | input.addEventListener( 'change', function () { uploadFiles( input.files ); input.value = ''; } ); | ||
drop.addEventListener( 'drop', function ( e ) { | |||
input.addEventListener( 'change', function () { uploadFiles( input.files ); } ); | |||
function uploadFiles( fileList ) { | function uploadFiles( fileList ) { | ||
var files = Array.from( fileList ); | var files = Array.from( fileList ); | ||
var total = files.length, done = 0, | var total = files.length, done = 0, renamed = []; | ||
status.innerHTML = '<span class="gal-progress">上传中 0/' + total + '…</span>'; | status.innerHTML = '<span class="gal-progress">上传中 0/' + total + '…</span>'; | ||
api.getToken( 'csrf' ).done( function ( token ) { | |||
/* Upload sequentially to avoid token races */ | |||
files.reduce( function ( chain, file ) { | |||
return chain.then( function () { | |||
return uploadOne( file, token, 0 ).then( function ( res ) { | |||
done++; | |||
if ( res.ok && res.filename !== file.name ) { | |||
renamed.push( file.name + ' → ' + res.filename ); | |||
} | |||
var progress = '上传中 ' + done + '/' + total + '…'; | |||
if ( renamed.length ) { | |||
progress += '<br><span class="gal-rename-note">已自动重命名:' + | |||
renamed.join( ',' ) + '</span>'; | |||
} | |||
if ( !res.ok ) { | |||
progress += '<br><span class="gal-err">' + res.filename + ' 失败:' + res.error + '</span>'; | |||
} | |||
status.innerHTML = '<span class="gal-progress">' + progress + '</span>'; | |||
if ( done === total ) { | |||
var summary = '✓ 全部上传完成!'; | |||
if ( renamed.length ) { | |||
summary += '<br><span class="gal-rename-note">以下文件因重名已自动重命名:<br>' + | |||
renamed.join( '<br>' ) + '</span>'; | |||
} | |||
status.innerHTML = '<span class="gal-ok">' + summary + '</span>'; | |||
reload(); | |||
} | |||
} ); | |||
} ); | } ); | ||
} ); | }, Promise.resolve() ); | ||
} ); | } ); | ||
} | } | ||
| 第176行: | 第218行: | ||
fetchImages( 8, function ( pages ) { | fetchImages( 8, function ( pages ) { | ||
if ( pages.length | if ( !pages.length ) { | ||
root.innerHTML = '<p class="gal-widget-empty">图库暂无图片</p>' + | root.innerHTML = '<p class="gal-widget-empty">图库暂无图片</p>' + | ||
'<a href="' + mw.util.getUrl( PAGE_NAME ) + '">前往图库上传 →</a>'; | '<a href="' + mw.util.getUrl( PAGE_NAME ) + '">前往图库上传 →</a>'; | ||
2026年3月12日 (四) 13:13的版本
/**
* Gallery gadget — Instagram-style image gallery with drag-and-drop upload.
* Powers the full 图库 page and the homepage mini-widget.
*/
( function () {
'use strict';
var CAT = 'Category:图库';
var PAGE_NAME = '图库';
var api = new mw.Api();
/* ── Fetch images from the 图库 category ─────────────────────── */
function fetchImages( limit, cb ) {
api.get( {
action: 'query',
generator: 'categorymembers',
gcmtitle: CAT, gcmtype: 'file',
gcmlimit: limit, gcmsort: 'timestamp', gcmdir: 'descending',
prop: 'imageinfo',
iiprop: 'url|dimensions|timestamp',
iiurlwidth: 400,
format: 'json'
} ).done( function ( data ) {
if ( !data.query || !data.query.pages ) { cb( [] ); return; }
var pages = Object.values( data.query.pages ).filter( function ( p ) {
return p.imageinfo && p.imageinfo[ 0 ];
} );
pages.sort( function ( a, b ) {
return b.imageinfo[ 0 ].timestamp.localeCompare( a.imageinfo[ 0 ].timestamp );
} );
cb( pages );
} ).fail( function () { cb( [] ); } );
}
/* ── Build one grid item ────────────────────────────────────────*/
function makeItem( page, cls ) {
var info = page.imageinfo[ 0 ];
var thumb = info.thumburl || info.url;
var title = page.title;
var href = mw.util.getUrl( title );
var a = document.createElement( 'a' );
a.href = href;
a.className = cls + ' image';
a.title = title.replace( /^(File|文件):/, '' );
var img = document.createElement( 'img' );
img.src = thumb;
img.alt = a.title;
img.loading = 'lazy';
a.appendChild( img );
return a;
}
/* ── Upload a single file, auto-renaming on conflict ────────────
* Strategy: try original name → on exists/duplicate warning,
* insert "_N" before the extension and retry (up to 99 times).
* ─────────────────────────────────────────────────────────────── */
function uploadOne( file, token, attempt ) {
attempt = attempt || 0;
/* Build filename with optional suffix */
var filename;
if ( attempt === 0 ) {
filename = file.name;
} else {
var m = file.name.match( /^(.*?)(\.[^.]+)?$/ );
var base = m[ 1 ] || file.name;
var ext = m[ 2 ] || '';
filename = base + '_' + attempt + ext;
}
if ( attempt > 99 ) {
return Promise.resolve( { ok: false, filename: filename, error: '重命名次数超限' } );
}
var fd = new FormData();
fd.append( 'action', 'upload' );
fd.append( 'format', 'json' );
fd.append( 'filename', filename );
fd.append( 'file', file );
fd.append( 'token', token );
/* No ignorewarnings — let MediaWiki tell us about conflicts */
fd.append( 'text', '[[Category:图库]]\n上传自图库。' );
fd.append( 'comment', '图库上传' );
return fetch( mw.config.get( 'wgScriptPath' ) + '/api.php', {
method: 'POST', body: fd, credentials: 'same-origin'
} )
.then( function ( r ) { return r.json(); } )
.then( function ( res ) {
/* Success */
if ( res.upload && res.upload.result === 'Success' ) {
return { ok: true, filename: filename };
}
/* Conflict warnings → retry with next suffix */
var w = res.upload && res.upload.warnings;
if ( w && ( w.exists || w[ 'page-exists' ] || w.duplicate || w.badfilename ) ) {
return uploadOne( file, token, attempt + 1 );
}
/* Any other error */
var msg = ( res.error && res.error.info ) ||
( w && JSON.stringify( w ) ) || '未知错误';
return { ok: false, filename: filename, error: msg };
} )
.catch( function () {
return { ok: false, filename: filename, error: '网络错误' };
} );
}
/* ════════════════════════════════════════════════════════════════
FULL GALLERY PAGE
════════════════════════════════════════════════════════════════ */
function initFullGallery() {
var root = document.getElementById( 'mw-gallery-root' );
if ( !root ) { return; }
var groups = mw.config.get( 'wgUserGroups' ) || [];
var canUpload = groups.indexOf( 'user' ) !== -1 || groups.indexOf( 'sysop' ) !== -1;
root.innerHTML =
( canUpload
? '<div id="gal-drop">' +
' <div class="gal-drop-inner">' +
' <span class="gal-drop-icon">📷</span>' +
' <p>将图片拖放至此上传</p>' +
' <p class="gal-drop-sub">或 <label class="gal-browse">点击选择文件' +
' <input type="file" id="gal-file-input" multiple accept="image/*">' +
' </label></p>' +
' </div>' +
' <div id="gal-status"></div>' +
'</div>'
: '' ) +
'<div id="gal-grid" class="gal-grid"></div>';
var grid = document.getElementById( 'gal-grid' );
var status = document.getElementById( 'gal-status' );
function reload() {
grid.innerHTML = '<div class="gal-loading">加载中…</div>';
fetchImages( 200, function ( pages ) {
grid.innerHTML = '';
if ( !pages.length ) {
grid.innerHTML = '<div class="gal-empty">还没有图片,快来上传第一张吧!</div>';
return;
}
pages.forEach( function ( p ) { grid.appendChild( makeItem( p, 'gal-item' ) ); } );
} );
}
reload();
if ( !canUpload ) { return; }
var drop = document.getElementById( 'gal-drop' );
var input = document.getElementById( 'gal-file-input' );
drop.addEventListener( 'dragover', function ( e ) { e.preventDefault(); drop.classList.add( 'gal-dragover' ); } );
drop.addEventListener( 'dragleave', function () { drop.classList.remove( 'gal-dragover' ); } );
drop.addEventListener( 'drop', function ( e ) { e.preventDefault(); drop.classList.remove( 'gal-dragover' ); uploadFiles( e.dataTransfer.files ); } );
input.addEventListener( 'change', function () { uploadFiles( input.files ); input.value = ''; } );
function uploadFiles( fileList ) {
var files = Array.from( fileList );
var total = files.length, done = 0, renamed = [];
status.innerHTML = '<span class="gal-progress">上传中 0/' + total + '…</span>';
api.getToken( 'csrf' ).done( function ( token ) {
/* Upload sequentially to avoid token races */
files.reduce( function ( chain, file ) {
return chain.then( function () {
return uploadOne( file, token, 0 ).then( function ( res ) {
done++;
if ( res.ok && res.filename !== file.name ) {
renamed.push( file.name + ' → ' + res.filename );
}
var progress = '上传中 ' + done + '/' + total + '…';
if ( renamed.length ) {
progress += '<br><span class="gal-rename-note">已自动重命名:' +
renamed.join( ',' ) + '</span>';
}
if ( !res.ok ) {
progress += '<br><span class="gal-err">' + res.filename + ' 失败:' + res.error + '</span>';
}
status.innerHTML = '<span class="gal-progress">' + progress + '</span>';
if ( done === total ) {
var summary = '✓ 全部上传完成!';
if ( renamed.length ) {
summary += '<br><span class="gal-rename-note">以下文件因重名已自动重命名:<br>' +
renamed.join( '<br>' ) + '</span>';
}
status.innerHTML = '<span class="gal-ok">' + summary + '</span>';
reload();
}
} );
} );
}, Promise.resolve() );
} );
}
}
/* ════════════════════════════════════════════════════════════════
HOMEPAGE MINI WIDGET
════════════════════════════════════════════════════════════════ */
function initWidget() {
var root = document.getElementById( 'mw-gallery-widget' );
if ( !root ) { return; }
root.innerHTML = '<div class="gal-widget-grid"></div>' +
'<a href="' + mw.util.getUrl( PAGE_NAME ) + '" class="gal-widget-more">查看全部 →</a>';
var grid = root.querySelector( '.gal-widget-grid' );
fetchImages( 8, function ( pages ) {
if ( !pages.length ) {
root.innerHTML = '<p class="gal-widget-empty">图库暂无图片</p>' +
'<a href="' + mw.util.getUrl( PAGE_NAME ) + '">前往图库上传 →</a>';
return;
}
pages.slice( 0, 8 ).forEach( function ( p ) {
grid.appendChild( makeItem( p, 'gal-widget-item' ) );
} );
} );
}
$( function () {
if ( mw.config.get( 'wgPageName' ) === PAGE_NAME ) { initFullGallery(); }
initWidget();
} );
}() );