跳转到内容

User:Addonian1123/QuickRating.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// <nowiki>
/**
 * QuickRating
 * 為模板與模組提供快速新增、修改或清除評級模板的工具

var _addText = "{{工具警告|QuickRating|page=程式碼}}";
var _addText = "{{Notice|<big>'''文檔請見:[[User:Addonian1123/QuickRating]]'''</big>}}";
 */

$(() => {
    // 僅在模板(10, 11)與模組(828, 829)命名空間執行
    const ns = mw.config.get('wgNamespaceNumber');
    if (![10, 11, 828, 829].includes(ns)) return;

    // 新增工具列按鈕
    const portletLink = mw.util.addPortletLink(
        'p-cactions',
        '#',
        '評級此模板/模組',
        'ca-rating-tool',
        '[QuickRating] 為此模板或模組評級'
    );
    if (!portletLink) return;

    $(portletLink).on('click', (e) => {
        e.preventDefault();
        mw.loader.using(['mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).then(initTool);
    });

    // 階段評級對照表
    const phaseAliasMap = {
        'pre-alpha': 'pre-alpha', 'prealpha': 'pre-alpha', 'pa': 'pre-alpha',
        'alpha': 'alpha', 'a': 'alpha',
        'beta': 'beta', 'b': 'beta',
        'release': 'release', 'r': 'release', 'general': 'release', 'g': 'release', 'stable': 'release',
        'broken': 'broken', 'br': 'broken', 'unstable': 'broken',
        'deprecated': 'deprecated', 'd': 'deprecated', 'defunct': 'deprecated'
    };

    // 保護狀態對照表
    const protectionAliasMap = {
        'semiprotected': 'semiprotected', 'semiprotect': 'semiprotected', 'semi': 'semiprotected',
        'protected': 'protected', 'protect': 'protected', 'p': 'protected'
    };

    function initTool() {
        let titleObj = new mw.Title(mw.config.get('wgPageName'));
        if (titleObj.isTalkPage()) {
            titleObj = titleObj.getSubjectPage();
        }
        
        const pageName = titleObj.getPrefixedText();
        const api = new mw.Api();

        api.get({
            action: 'query',
            prop: 'revisions',
            rvprop: 'content',
            rvslots: 'main',
            titles: pageName,
            formatversion: 2
        }).done((data) => {
            const page = data?.query?.pages?.[0];
            const content = (page && !page.missing) ? (page.revisions?.[0]?.slots?.main?.content || '') : '';

            const isDocPage = pageName.endsWith('/doc') || /\{\{\s*(Documentation subpage|模板文档子页面|說明文件子頁面)/i.test(content);
            let targetDocName = pageName;

            if (isDocPage) {
                showDialog(api, targetDocName, content);
            } else {
                const cleanContent = content.replace(/<!--[\s\S]*?-->/g, '').replace(/<nowiki>[\s\S]*?<\/nowiki>/gi, '');
                const docAliases = 'Documentation|Doc|Documentation, template|Documentations|Template doc|Template doc page|Template doc page transcluded|模板文件|模板文档|說明文件';
                const docRegex = new RegExp(`\\{\\{\\s*(${docAliases})\\s*(\\|[^\\}]*)?\\}\\}`, 'i');
                const match = cleanContent.match(docRegex);
                
                targetDocName = `${pageName}/doc`;

                if (match?.[2]) {
                    const params = match[2].split('|');
                    for (let i = 1; i < params.length; i++) {
                        const param = params[i].trim();
                        if (param.startsWith('1=')) {
                            targetDocName = param.substring(2).trim();
                            break;
                        } else if (!param.includes('=')) {
                            targetDocName = param;
                            break;
                        }
                    }
                }

                api.get({
                    action: 'query',
                    prop: 'revisions',
                    rvprop: 'content',
                    rvslots: 'main',
                    titles: targetDocName,
                    formatversion: 2
                }).done((docData) => {
                    const docPage = docData?.query?.pages?.[0];
                    const docContent = (docPage && !docPage.missing) ? (docPage.revisions?.[0]?.slots?.main?.content || '') : '';
                    showDialog(api, targetDocName, docContent);
                }).fail((err) => {
                    OO.ui.alert(`無法讀取子頁面內容:${err}`);
                });
            }
        }).fail((err) => {
            OO.ui.alert(`無法讀取頁面內容:${err}`);
        });
    }

    function showDialog(api, targetDocName, docContent) {
        const isModule = /^(Module|模组|模組):/i.test(targetDocName) || [828, 829].includes(ns);
        const templateName = isModule ? 'Module rating' : 'Template rating';

        const phaseOptions = [
            { data: 'pre-alpha', label: 'Pre-Alpha版' },
            { data: 'alpha', label: 'Alpha版' },
            { data: 'beta', label: 'Beta測試版' },
            { data: 'release', label: '穩定版' },
            { data: 'broken', label: '已損壞' },
            { data: 'deprecated', label: '已廢棄' }
        ];

        const protectionOptions = [
            { data: 'semiprotected', label: '半保護' },
            { data: 'protected', label: '全保護' }
        ];

        // 偵測頁面中所有現存評級模板參數
        const existingRegexGlobal = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|([^}]+)\\}\\}`, 'gi');
        const detectedParamsList = [];
        let match;
        while ((match = existingRegexGlobal.exec(docContent)) !== null) {
            detectedParamsList.push(match[1].trim());
        }

        const initialParam1 = detectedParamsList[0] || '';
        const initialParam2 = detectedParamsList[1] || '';

        function CustomRatingDialog(config) {
            CustomRatingDialog.super.call(this, config);
        }
        OO.inheritClass(CustomRatingDialog, OO.ui.ProcessDialog);

        CustomRatingDialog.static.name = 'customRatingDialog';
        CustomRatingDialog.static.size = 'medium';
        CustomRatingDialog.static.title = new OO.ui.HtmlSnippet(
            `<a href="${mw.util.getUrl('User:Addonian1123/QuickRating')}" target="_blank" style="color: inherit; text-decoration: underline;">QuickRating</a>:設定${isModule ? '模組' : '模板'}評級`
        );
        CustomRatingDialog.static.actions = [
            { action: 'save', label: '儲存變更', flags: ['primary', 'progressive'] },
            { action: 'close', icon: 'close', flags: ['safe', 'close'], title: '關閉' }
        ];

        CustomRatingDialog.prototype.initialize = function () {
            CustomRatingDialog.super.prototype.initialize.apply(this, arguments);

            this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });

            // 目標頁面輸入框
            this.targetInput = new OO.ui.TextInputWidget({ value: targetDocName });

            // 階段評級 RadioSelect
            this.phaseRadio = new OO.ui.RadioSelectWidget();
            phaseOptions.forEach(opt => {
                this.phaseRadio.addItems([
                    new OO.ui.RadioOptionWidget({ data: opt.data, label: opt.label })
                ]);
            });

            // 特殊狀態 Checkbox 與 RadioSelect
            this.specialCheckbox = new OO.ui.CheckboxInputWidget({ selected: false });
            this.protectionRadio = new OO.ui.RadioSelectWidget();
            protectionOptions.forEach(opt => {
                this.protectionRadio.addItems([
                    new OO.ui.RadioOptionWidget({ data: opt.data, label: opt.label })
                ]);
            });

            // 自訂參數輸入框
            this.paramInput1 = new OO.ui.TextInputWidget({
                value: initialParam1,
                placeholder: '例如:release|nocat=true'
            });
            this.paramInput2 = new OO.ui.TextInputWidget({
                value: initialParam2,
                placeholder: '例如:protected|nocat=true'
            });

            // 初始化表單狀態
            this.parseInitialState(initialParam1, initialParam2);

            // 單向點擊事件:僅在點選 Radio 時填入首項參數
            this.phaseRadio.on('select', (item) => {
                if (!item) return;
                const val = item.getData();
                const currentText = this.paramInput1.getValue().trim();
                if (!currentText) {
                    this.paramInput1.setValue(val);
                } else {
                    const parts = currentText.split('|').map(s => s.trim());
                    let replaced = false;
                    for (let i = 0; i < parts.length; i++) {
                        if (phaseAliasMap[parts[i].toLowerCase()]) {
                            parts[i] = val;
                            replaced = true;
                            break;
                        }
                    }
                    if (!replaced) parts.unshift(val);
                    this.paramInput1.setValue(parts.join('|'));
                }
            });

            this.protectionRadio.on('select', (item) => {
                if (!item || !this.specialCheckbox.isSelected()) return;
                const val = item.getData();
                const currentText = this.paramInput2.getValue().trim();
                if (!currentText) {
                    this.paramInput2.setValue(val);
                } else {
                    const parts = currentText.split('|').map(s => s.trim());
                    let replaced = false;
                    for (let i = 0; i < parts.length; i++) {
                        if (protectionAliasMap[parts[i].toLowerCase()]) {
                            parts[i] = val;
                            replaced = true;
                            break;
                        }
                    }
                    if (!replaced) parts.unshift(val);
                    this.paramInput2.setValue(parts.join('|'));
                }
            });

            // 切換特殊狀態開關
            const toggleSpecial = (enabled) => {
                this.protectionRadio.setDisabled(!enabled);
                this.paramInput2.setDisabled(!enabled);
                if (enabled && !this.paramInput2.getValue().trim()) {
                    const selected = this.protectionRadio.findSelectedItem();
                    if (selected) {
                        this.paramInput2.setValue(selected.getData());
                    } else {
                        this.protectionRadio.selectItemByData('semiprotected');
                        this.paramInput2.setValue('semiprotected');
                    }
                }
            };

            this.specialCheckbox.on('change', toggleSpecial);
            toggleSpecial(this.specialCheckbox.isSelected());

            // 清除全部模板按鈕
            const clearAllBtn = new OO.ui.ButtonWidget({
                label: '清除全部模板', flags: ['destructive'], framed: true
            });
            clearAllBtn.$element.css({ 'float': 'left', 'margin-top': '12px' });

            clearAllBtn.on('click', () => {
                const customTarget = this.targetInput.getValue().trim();
                document.activeElement?.blur();
                const manager = this.getManager();
                this.close().closed.then(() => {
                    manager?.destroy();
                    if (!customTarget) {
                        OO.ui.alert('請輸入有效的目標頁面名稱。').done(() => showDialog(api, targetDocName, docContent));
                        return;
                    }

                    OO.ui.confirm(`確定要清除「${customTarget}」上的所有評級模板嗎?`).done((confirmed) => {
                        if (confirmed) {
                            saveRating(api, templateName, [], customTarget, docContent, null);
                        } else {
                            showDialog(api, targetDocName, docContent);
                        }
                    });
                });
            });

            // 表單組裝
            const fieldset = new OO.ui.FieldsetLayout({ label: '評級設定' });
            fieldset.addItems([
                new OO.ui.FieldLayout(this.targetInput, {
                    label: `目標${isModule ? '模組' : '模板'}子頁面:`,
                    align: 'top'
                }),
                new OO.ui.FieldLayout(this.phaseRadio, {
                    label: '階段評級:',
                    align: 'top'
                }),
                new OO.ui.FieldLayout(this.specialCheckbox, {
                    label: '特殊狀態',
                    align: 'inline'
                }),
                new OO.ui.FieldLayout(this.protectionRadio, {
                    label: '保護狀態(僅在「特殊狀態」啟用時生效):',
                    align: 'top'
                }),
                new OO.ui.FieldLayout(this.paramInput1, {
                    label: '階段評級自訂參數:',
                    align: 'top'
                }),
                new OO.ui.FieldLayout(this.paramInput2, {
                    label: '特殊狀態自訂參數(僅在「特殊狀態」啟用時生效):',
                    align: 'top'
                })
            ]);

            const footerContainer = $('<div>').css({ 'clear': 'both' }).append(clearAllBtn.$element);

            this.panel.$element.append(fieldset.$element, footerContainer);
            this.$body.append(this.panel.$element);
        };

        CustomRatingDialog.prototype.parseInitialState = function (p1, p2) {
            let matchedPhase = null;
            let matchedProt = null;

            if (p1) {
                const parts = p1.split('|').map(s => s.trim());
                for (const part of parts) {
                    const lower = part.toLowerCase();
                    if (phaseAliasMap[lower]) {
                        matchedPhase = phaseAliasMap[lower];
                        break;
                    }
                    if (!matchedProt && protectionAliasMap[lower]) {
                        matchedProt = protectionAliasMap[lower];
                    }
                }
            }

            if (p2) {
                const parts = p2.split('|').map(s => s.trim());
                for (const part of parts) {
                    const lower = part.toLowerCase();
                    if (protectionAliasMap[lower]) {
                        matchedProt = protectionAliasMap[lower];
                        break;
                    }
                }
            }

            if (matchedPhase) {
                this.phaseRadio.selectItemByData(matchedPhase);
            }

            if (p2 || matchedProt) {
                this.specialCheckbox.setSelected(true);
                if (matchedProt) {
                    this.protectionRadio.selectItemByData(matchedProt);
                }
            }
        };

        CustomRatingDialog.prototype.getActionProcess = function (action) {
            if (action === 'save') {
                return new OO.ui.Process(() => {
                    const customTarget = this.targetInput.getValue().trim();
                    if (!customTarget) {
                        return Promise.reject(new OO.ui.Error('請輸入有效的目標頁面名稱。', { warning: true }));
                    }

                    const val1 = this.paramInput1.getValue().trim();
                    const val2 = this.paramInput2.getValue().trim();
                    const isSpecial = this.specialCheckbox.isSelected();

                    if (!isSpecial && val2) {
                        return Promise.reject(new OO.ui.Error('若要設定第二個自訂參數,請先勾選「特殊狀態」。', { warning: true }));
                    }

                    const paramStrings = [];
                    if (val1) paramStrings.push(val1);
                    if (isSpecial && val2) paramStrings.push(val2);

                    return saveRating(api, templateName, paramStrings, customTarget, docContent, this);
                });
            }
            if (action === 'close') {
                return new OO.ui.Process(() => {
                    document.activeElement?.blur();
                    const manager = this.getManager();
                    return this.close().closed.then(() => {
                        manager?.destroy();
                    });
                });
            }
            return CustomRatingDialog.super.prototype.getActionProcess.call(this, action);
        };

        const windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);

        const dialog = new CustomRatingDialog();
        windowManager.addWindows([dialog]);
        windowManager.openWindow(dialog);
    }

    function saveRating(api, templateName, allParamStrings, targetDocName, actualContent, dialog) {
        const existingRegexGlobal = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\}\\n?`, 'gi');
        let newContent = actualContent;

        if (allParamStrings.length === 0) {
            if (existingRegexGlobal.test(actualContent)) {
                existingRegexGlobal.lastIndex = 0;
                newContent = actualContent.replace(existingRegexGlobal, '').replace(/\n\n\n+/g, '\n\n').trim();
            } else {
                if (dialog) {
                    document.activeElement?.blur();
                    const manager = dialog.getManager();
                    dialog.close().closed.then(() => manager?.destroy());
                }
                OO.ui.alert('該頁面目前未掛有評級模板,未進行任何變更。').done(() => {
                    showDialog(api, targetDocName, actualContent);
                });
                return Promise.resolve();
            }
        } else {
            const generatedTemplates = allParamStrings.map(p => `{{${templateName}|${p}}}`);
            const newTplBlock = `${generatedTemplates.join('')}`;
            const replaceRegex = new RegExp(`(\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\})+`, 'i');

            if (replaceRegex.test(actualContent)) {
                newContent = actualContent.replace(replaceRegex, newTplBlock);
            } else {
                const docSubRegex = /\{\{\s*(Documentation subpage|模板文档子页面|說明文件子頁面)[^\\}]*\}\}\n?/i;
                if (docSubRegex.test(actualContent)) {
                    newContent = actualContent.replace(docSubRegex, `$&${newTplBlock}`);
                } else {
                    newContent = newTplBlock + actualContent;
                }
            }
        }

        const isExisting = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\}`, 'i').test(actualContent);
        const actionText = isExisting ? '變更' : '新增';

        const summaryText = (allParamStrings.length === 0)
            ? `清除所有評級模板([[Template:${templateName}]])// [[User:Addonian1123/QuickRating|QuickRating]]`
            : `${actionText}評級模板([[Template:${templateName}]])為:${allParamStrings.join(' / ')} // [[User:Addonian1123/QuickRating|QuickRating]]`;

        return api.postWithToken('csrf', {
            action: 'edit',
            title: targetDocName,
            text: newContent,
            summary: summaryText
        }).then(() => {
            if (dialog) {
                document.activeElement?.blur();
                const manager = dialog.getManager();
                dialog.close().closed.then(() => manager?.destroy());
            }
            const successMsg = (allParamStrings.length === 0 ? '已成功清除所有評級模板。' : `已成功${actionText}評級模板。`);
            showCompletionDialog(api, targetDocName, successMsg);
        }, (err) => {
            if (dialog) {
                document.activeElement?.blur();
                const manager = dialog.getManager();
                dialog.close().closed.then(() => manager?.destroy());
            }
            OO.ui.alert(`儲存失敗:${err}`).done(() => {
                showDialog(api, targetDocName, actualContent);
            });
        });
    }

    function showCompletionDialog(api, targetDocName, messageText) {
        const windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);
        const messageDialog = new OO.ui.MessageDialog();
        windowManager.addWindows([messageDialog]);
        windowManager.openWindow(messageDialog, {
            title: '操作成功',
            message: messageText,
            actions: [
                { action: 'purge', label: '清除快取', flags: ['primary', 'progressive'] },
                { action: 'reload', label: '重新載入頁面', flags: ['safe'] }
            ]
        }).closed.then((data) => {
            document.activeElement?.blur();
            windowManager.destroy();
            if (data?.action === 'purge') {
                mw.notify('正在清除頁面快取...');
                api.post({
                    action: 'purge',
                    titles: [mw.config.get('wgPageName'), targetDocName],
                    forcelinkupdate: 1
                }).always(() => {
                    location.reload();
                });
            } else {
                location.reload();
            }
        });
    }
});
// </nowiki>