跳转到内容

MediaWiki:Gadget-Gallery.js:修订间差异

来自槌基百科
Create Instagram-style gallery gadget
 
Auto-rename on duplicate filename instead of overwriting
第33行: 第33行:
     }
     }


     /* ── Build one grid item (works for both full and widget grids) ─ */
     /* ── 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;                         // "File:foo.jpg"
         var title = page.title;
         var href  = mw.util.getUrl( title );
         var href  = mw.util.getUrl( title );


         var a   = document.createElement( 'a' );
         var a       = document.createElement( 'a' );
         a.href = href;
         a.href     = href;
         a.className = cls + ' image';                   // "image" lets MultimediaViewer intercept
         a.className = cls + ' image';
         a.title = title.replace( /^(File|文件):/, '' );
         a.title     = title.replace( /^(File|文件):/, '' );


         var img   = document.createElement( 'img' );
         var img     = document.createElement( 'img' );
         img.src   = thumb;
         img.src     = thumb;
         img.alt   = a.title;
         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;


         /* Build skeleton */
         root.innerHTML =
        if ( canUpload ) {
            ( canUpload
            root.innerHTML =
                 ? '<div id="gal-drop">' +
                 '<div id="gal-drop">' +
                  '  <div class="gal-drop-inner">' +
                '  <div class="gal-drop-inner">' +
                  '    <span class="gal-drop-icon">📷</span>' +
                '    <span class="gal-drop-icon">📷</span>' +
                  '    <p>将图片拖放至此上传</p>' +
                '    <p>将图片拖放至此上传</p>' +
                  '    <p class="gal-drop-sub">或 <label class="gal-browse">点击选择文件' +
                '    <p class="gal-drop-sub">或 <label class="gal-browse">点击选择文件' +
                  '      <input type="file" id="gal-file-input" multiple accept="image/*">' +
                '      <input type="file" id="gal-file-input" multiple accept="image/*">' +
                  '    </label></p>' +
                '    </label></p>' +
                  '  </div>' +
                '  </div>' +
                  '  <div id="gal-status"></div>' +
                '  <div id="gal-status"></div>' +
                  '</div>'
                '</div>' +
                 : '' ) +
                 '<div id="gal-grid" class="gal-grid"></div>';
             '<div id="gal-grid" class="gal-grid"></div>';
        } else {
             root.innerHTML = '<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 === 0 ) {
                 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' ) ); } );
                    grid.appendChild( makeItem( p, 'gal-item' ) );
                } );
             } );
             } );
         }
         }
第102行: 第155行:
         if ( !canUpload ) { return; }
         if ( !canUpload ) { return; }


        /* ── Upload logic ─────────────────────────────────────────── */
         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' ); } );
            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.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 ); } );


         function uploadFiles( fileList ) {
         function uploadFiles( fileList ) {
             var files = Array.from( fileList );
             var files = Array.from( fileList );
             var total = files.length, done = 0, failed = 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>';


             files.forEach( function ( file ) {
             api.getToken( 'csrf' ).done( function ( token ) {
                api.getToken( 'csrf' ).done( function ( token ) {
                /* Upload sequentially to avoid token races */
                    var fd = new FormData();
                files.reduce( function ( chain, file ) {
                     fd.append( 'action',         'upload' );
                     return chain.then( function () {
                    fd.append( 'format',        'json' );
                        return uploadOne( file, token, 0 ).then( function ( res ) {
                    fd.append( 'filename',      file.name );
                            done++;
                    fd.append( 'file',          file );
                            if ( res.ok && res.filename !== file.name ) {
                    fd.append( 'token',          token );
                                renamed.push( file.name + ' ' + res.filename );
                    fd.append( 'ignorewarnings', '1' );
                            }
                    fd.append( 'text',          '[[Category:图库]]\n上传自图库。' );
 
                    fd.append( 'comment',        '图库上传' );
                            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>';


                    fetch( mw.config.get( 'wgScriptPath' ) + '/api.php', {
                            if ( done === total ) {
                        method: 'POST', body: fd, credentials: 'same-origin'
                                var summary = '✓ 全部上传完成!';
                    } )
                                if ( renamed.length ) {
                    .then( function ( r ) { return r.json(); } )
                                    summary += '<br><span class="gal-rename-note">以下文件因重名已自动重命名:<br>' +
                    .then( function ( res ) {
                                        renamed.join( '<br>' ) + '</span>';
                        done++;
                                 }
                        if ( !res.upload || res.upload.result !== 'Success' ) { failed++; }
                                status.innerHTML = '<span class="gal-ok">' + summary + '</span>';
                        var msg = done < total
                                reload();
                            ? '上传中 ' + done + '/' + total + '…'
                            }
                            : ( failed
                        } );
                                ? '完成,' + failed + ' 张失败'
                                 : '✓ 全部上传完成!' );
                        status.innerHTML = '<span class="' +
                            ( done === total && !failed ? 'gal-ok' : 'gal-progress' ) +
                            '">' + msg + '</span>';
                        if ( done === total ) { reload(); }
                    } )
                    .catch( function () {
                        done++; failed++;
                        status.innerHTML = '<span class="gal-err">上传出错,请重试</span>';
                     } );
                     } );
                 } );
                 }, Promise.resolve() );
             } );
             } );
         }
         }
第176行: 第218行:


         fetchImages( 8, function ( pages ) {
         fetchImages( 8, function ( pages ) {
             if ( pages.length === 0 ) {
             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();
    } );

}() );