User:DVRTed/script-ctl.js

mw.loader.using([ "vue", "@wikimedia/codex", "mediawiki.api", "mediawiki.util" ]).then(function() { const require =

mw.loader.using([
	"vue",
	"@wikimedia/codex",
	"mediawiki.api",
	"mediawiki.util"
]).then(function() {
	const require = mw.loader.require;
	(function(vue, _wikimedia_codex) {
		//#region node_modules/@wikimedia/codex-icons/dist/codex-icons.js
		var M = "<path d=\"M11 9V4H9v5H4v2h5v5h2v-5h5V9z\"/>", g = "<path d=\"M11.53 2.3A1.85 1.85 0 0010 1.21 1.85 1.85 0 008.48 2.3L.36 16.36C-.48 17.81.21 19 1.88 19h16.24c1.67 0 2.36-1.19 1.52-2.64zM11 16H9v-2h2zm0-4H9V6h2z\"/>", D = "<path d=\"M10 1a9 9 0 109 9 9 9 0 00-9-9m5 10H5V9h10z\"/>", v1 = "<path d=\"M7 14.17 2.83 10l-1.41 1.41L7 17 19 5l-1.41-1.42z\"/>", z1 = "<path d=\"m4.34 2.93 12.73 12.73-1.41 1.41L2.93 4.35z\"/><path d=\"M17.07 4.34 4.34 17.07l-1.41-1.41L15.66 2.93z\"/>", f1 = "<circle cx=\"10\" cy=\"10\" r=\"2\"/><circle cx=\"3\" cy=\"10\" r=\"2\"/><circle cx=\"17\" cy=\"10\" r=\"2\"/>", W2 = "<path d=\"m4 10 9 9 1.4-1.5L7 10l7.4-7.5L13 1z\"/>", _2 = "<path d=\"M15.65 4.35A8 8 0 1017.4 13h-2.22a6 6 0 11-1-7.22L11 9h7V2z\"/>", t5 = "<path d=\"M12.2 13.6a7 7 0 111.4-1.4l5.4 5.4-1.4 1.4zM3 8a5 5 0 1010 0A5 5 0 003 8\"/>", N5 = "<path d=\"M17 2h-3.5l-1-1h-5l-1 1H3v2h14zM4 17a2 2 0 002 2h8a2 2 0 002-2V5H4z\"/>", C3 = M, F3 = g, Y3 = D, n4 = v1, r4 = z1, F4 = f1, C7 = {
			ltr: W2,
			shouldFlip: !0
		}, W7 = _2, P7 = t5, y8 = N5;
		//#endregion
		//#region src/helpers.js
		function build_raw_url(pagename, oldid) {
			const url = new URL(mw.config.get("wgScriptPath") + "/index.php", location.origin);
			url.searchParams.set("title", pagename);
			url.searchParams.set("action", "raw");
			url.searchParams.set("ctype", "text/javascript");
			if (oldid) url.searchParams.set("oldid", String(oldid));
			return url.toString();
		}
		function fetch_page_info(pagename) {
			return new mw.Api().get({
				action: "query",
				prop: "revisions|info",
				titles: pagename,
				rvprop: "ids",
				rvlimit: 1,
				formatversion: 2
			}).then((data) => {
				const page = data.query.pages[0];
				return {
					exists: !page.missing,
					oldid: page.missing ? null : page.revisions[0].revid,
					normalized_title: page.title,
					content_model: page.contentmodel
				};
			});
		}
		async function fetch_latest_revisions(pagenames) {
			const result = {};
			if (!pagenames.length) return result;
			for (let i = 0; i < pagenames.length; i += 50) {
				const chunk = pagenames.slice(i, i + 50);
				try {
					const data = await new mw.Api().get({
						action: "query",
						prop: "revisions",
						titles: chunk.join("|"),
						rvprop: "ids",
						formatversion: 2
					});
					for (const page of data.query.pages || []) if (!page.missing && page.revisions?.[0]) result[page.title] = page.revisions[0].revid;
				} catch (e) {
					console.error("Failed to fetch revisions batch", e);
				}
			}
			return result;
		}
		function load_script(pagename, oldid) {
			return mw.loader.getScript(build_raw_url(pagename, oldid));
		}
		async function search_scripts(query) {
			if (!query || query.length < 3) return [];
			try {
				return ((await new mw.Api().get({
					action: "query",
					list: "search",
					srsearch: `intitle:${query} intitle:.js`,
					srnamespace: "2|4|8",
					srlimit: 10,
					format: "json"
				}))?.query?.search || []).map((r) => r.title);
			} catch (e) {
				return [];
			}
		}
		//#endregion
		//#region \0plugin-vue:export-helper
		var _plugin_vue_export_helper_default = (sfc, props) => {
			const target = sfc.__vccOpts || sfc;
			for (const [key, val] of props) target[key] = val;
			return target;
		};
		//#endregion
		//#region src/components/InstallForm.vue
		var _hoisted_1$2 = { class: "smgr-install-view" };
		var _hoisted_2$2 = { class: "smgr-card smgr-install-card" };
		var _hoisted_3$2 = { class: "smgr-install-row" };
		var _hoisted_4$2 = { class: "smgr-browse-footer" };
		var InstallForm_default = /* @__PURE__ */ _plugin_vue_export_helper_default({
			__name: "InstallForm",
			props: { busy: {
				type: Boolean,
				default: false
			} },
			emits: ["install", "cancel"],
			setup(__props, { expose: __expose, emit: __emit }) {
				const props = __props;
				const emit = __emit;
				const install_input = (0, vue.ref)("");
				const selected_script = (0, vue.ref)(null);
				const suggestions = (0, vue.ref)([]);
				let timeout = null;
				const menu_items = (0, vue.computed)(() => {
					return suggestions.value.map((s) => ({
						value: s,
						label: s
					}));
				});
				const on_input = (val) => {
					if (timeout) clearTimeout(timeout);
					if (!val || val.length < 3) {
						suggestions.value = [];
						return;
					}
					timeout = setTimeout(async () => {
						suggestions.value = await search_scripts(val);
					}, 300);
				};
				const submit = () => {
					if (props.busy) return;
					const val = selected_script.value || install_input.value;
					if (val) emit("install", typeof val === "string" ? val : val.value);
				};
				const open_browse = () => {
					window.open(mw.util.getUrl("Wikipedia:User scripts/List"), "_blank");
				};
				__expose({
					clear_input: () => {
						install_input.value = "";
						selected_script.value = null;
						suggestions.value = [];
					},
					set_input: (val) => {
						install_input.value = val;
						selected_script.value = null;
					}
				});
				return (_ctx, _cache) => {
					return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$2, [(0, vue.createElementVNode)("div", _hoisted_2$2, [
						_cache[3] || (_cache[3] = (0, vue.createElementVNode)("label", { class: "smgr-label" }, "Script Page Name", -1)),
						(0, vue.createElementVNode)("div", _hoisted_3$2, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxLookup), {
							selected: selected_script.value,
							"onUpdate:selected": _cache[0] || (_cache[0] = ($event) => selected_script.value = $event),
							"input-value": install_input.value,
							"onUpdate:inputValue": _cache[1] || (_cache[1] = ($event) => install_input.value = $event),
							placeholder: "User:Foo/bar.js",
							class: "smgr-input",
							"menu-items": menu_items.value,
							onInput: on_input,
							disabled: __props.busy,
							onKeyup: (0, vue.withKeys)(submit, ["enter"])
						}, null, 8, [
							"selected",
							"input-value",
							"menu-items",
							"disabled"
						]), (0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), {
							action: "progressive",
							weight: "primary",
							onClick: submit,
							disabled: __props.busy
						}, {
							default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.busy ? "Installing..." : "Install"), 1)]),
							_: 1
						}, 8, ["disabled"])]),
						(0, vue.createElementVNode)("div", _hoisted_4$2, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), {
							weight: "quiet",
							action: "progressive",
							onClick: open_browse
						}, {
							default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(P7) }, null, 8, ["icon"]), _cache[2] || (_cache[2] = (0, vue.createTextVNode)("Browse community userscripts ", -1))]),
							_: 1
						})])
					])]);
				};
			}
		}, [["__scopeId", "data-v-05e069a8"]]);
		//#endregion
		//#region src/components/ScriptList.vue
		var _hoisted_1$1 = { class: "smgr-scripts-view" };
		var _hoisted_2$1 = {
			key: 0,
			class: "smgr-state"
		};
		var _hoisted_3$1 = ["href"];
		var _hoisted_4$1 = {
			key: 0,
			class: "smgr-update-chip"
		};
		var _hoisted_5$1 = {
			key: 1,
			class: "smgr-empty"
		};
		var _hoisted_6$1 = {
			key: 2,
			class: "smgr-state"
		};
		var ScriptList_default = /* @__PURE__ */ _plugin_vue_export_helper_default({
			__name: "ScriptList",
			props: {
				scripts: {
					type: Array,
					required: true
				},
				updates: {
					type: Object,
					required: true
				},
				timestamps: {
					type: Object,
					default: () => ({})
				},
				loading: {
					type: Boolean,
					required: true
				},
				busy_idx: {
					type: Number,
					default: null
				},
				all_disabled: {
					type: Boolean,
					default: false
				}
			},
			emits: [
				"toggle",
				"update",
				"uninstall",
				"add_new"
			],
			setup(__props, { emit: __emit }) {
				const props = __props;
				const emit = __emit;
				const columns = [
					{
						id: "script",
						label: "Script"
					},
					{
						id: "status",
						label: "Status"
					},
					{
						id: "version",
						label: "Version"
					},
					{
						id: "actions",
						label: "Action",
						align: "end"
					}
				];
				const tableData = (0, vue.computed)(() => props.scripts.map((s, idx) => ({
					...s,
					_idx: idx
				})));
				function get_menu_items(row) {
					return [
						{
							label: "Update",
							value: "update",
							icon: W7,
							disabled: !props.updates[row.pagename]
						},
						{
							label: row.status === "enabled" ? "Disable" : "Enable",
							value: "toggle",
							icon: row.status === "enabled" ? Y3 : n4
						},
						{
							label: "Remove",
							value: "uninstall",
							icon: y8,
							action: "destructive"
						}
					];
				}
				function on_select(value, idx) {
					if (value === "update") emit("update", idx);
					if (value === "toggle") emit("toggle", {
						idx,
						current_status: props.scripts[idx].status
					});
					if (value === "uninstall") emit("uninstall", idx);
				}
				const get_url = (pagename, oldid) => {
					if (typeof mw === "undefined" || !mw.util) return "#";
					return mw.util.getUrl(pagename, oldid ? { oldid } : {});
				};
				return (_ctx, _cache) => {
					return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$1, [__props.loading ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$1, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxProgressBar), { "aria-label": "Loading scripts..." })])) : ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_wikimedia_codex.CdxTable), {
						key: 1,
						caption: "Installed scripts",
						columns,
						data: tableData.value
					}, {
						"item-script": (0, vue.withCtx)(({ row }) => [(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["smgr-script-cell", { "smgr-dimmed": row.status === "disabled" || __props.all_disabled }]) }, [
							(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["smgr-stripe", row.status === "enabled" && !__props.all_disabled ? "smgr-stripe--on" : "smgr-stripe--off"]) }, null, 2),
							(0, vue.createElementVNode)("a", {
								href: get_url(row.pagename, row.oldid),
								target: "_blank",
								class: "smgr-pagename"
							}, (0, vue.toDisplayString)(row.pagename), 9, _hoisted_3$1),
							__props.updates[row.pagename] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_4$1, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(F3) }, null, 8, ["icon"]), _cache[0] || (_cache[0] = (0, vue.createTextVNode)(" Update available ", -1))])) : (0, vue.createCommentVNode)("", true)
						], 2)]),
						"item-status": (0, vue.withCtx)(({ row }) => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxInfoChip), { status: row.status === "enabled" && !__props.all_disabled ? "success" : "warning" }, {
							default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.all_disabled ? "Disabled (Global)" : row.status === "enabled" ? "Enabled" : "Disabled"), 1)]),
							_: 2
						}, 1032, ["status"])]),
						"item-version": (0, vue.withCtx)(({ row }) => [__props.timestamps[row.pagename] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.timestamps[row.pagename]), 1), row.oldid ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createTextVNode)(" (rev " + (0, vue.toDisplayString)(row.oldid) + ")", 1)], 64)) : (0, vue.createCommentVNode)("", true)], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_5$1, "—"))]),
						"item-actions": (0, vue.withCtx)(({ row }) => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxMenuButton), {
							selected: null,
							"menu-items": get_menu_items(row),
							disabled: __props.busy_idx === row._idx,
							"aria-label": "Actions for " + row.pagename,
							"onUpdate:selected": ($event) => on_select($event, row._idx)
						}, {
							default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(F4) }, null, 8, ["icon"])]),
							_: 1
						}, 8, [
							"menu-items",
							"disabled",
							"aria-label",
							"onUpdate:selected"
						])]),
						_: 1
					}, 8, ["data"])), !__props.loading && !__props.scripts.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6$1, " No scripts installed yet. ")) : (0, vue.createCommentVNode)("", true)]);
				};
			}
		}, [["__scopeId", "data-v-e878156a"]]);
		//#endregion
		//#region src/script_store.js
		var BLOCK_RE = /\/\* scriptmanager:begin !DO NOT EDIT THIS LINE MANUALLY!\*\/([\s\S]*?)\/\* scriptmanager:end !DO NOT EDIT THIS LINE MANUALLY!\*\//;
		var COMMON_JS = "User:" + mw.config.get("wgUserName") + "/common.js";
		var SCRIPT_TAG = " (using [[User:DVRTed/script-ctl.js|script-ctl]])";
		var script_store = {
			async load() {
				const page = (await new mw.Api().get({
					action: "query",
					prop: "revisions",
					titles: COMMON_JS,
					rvprop: "content",
					rvslots: "main",
					formatversion: 2
				})).query.pages[0];
				if (page.missing) return [];
				const match = page.revisions[0].slots.main.content.match(BLOCK_RE);
				if (!match) return [];
				try {
					const match_without_comments = match[1].replace(/^\s*\/\/.*$/gm, "");
					return JSON.parse(match_without_comments.trim());
				} catch {
					return [];
				}
			},
			async save(list, summary = "Updated script list") {
				const api = new mw.Api();
				const page = (await api.get({
					action: "query",
					prop: "revisions",
					titles: COMMON_JS,
					rvprop: "content",
					rvslots: "main",
					formatversion: 2
				})).query.pages[0];
				const old_content = page.missing ? "" : page.revisions[0].slots.main.content;
				const block = `/* scriptmanager:begin !DO NOT EDIT THIS LINE MANUALLY!*/\n[\n${list.map((item) => {
					return `\t// backlink: [[${item.pagename}]]\n\t${JSON.stringify(item)}`;
				}).join(",\n\n")}\n]\n/* scriptmanager:end !DO NOT EDIT THIS LINE MANUALLY!*/`;
				const new_content = old_content.match(BLOCK_RE) ? old_content.replace(BLOCK_RE, block) : old_content + (old_content ? "\n\n" : "") + block;
				return api.postWithToken("csrf", {
					action: "edit",
					title: COMMON_JS,
					text: new_content,
					summary: summary + SCRIPT_TAG
				});
			},
			async add(pagename, oldid) {
				const list = await this.load();
				list.push({
					pagename,
					oldid,
					status: "enabled"
				});
				await this.save(list, `Installed [[${pagename}]]`);
			},
			async update(idx, oldid) {
				const list = await this.load();
				const old_oldid = list[idx].oldid;
				list[idx].oldid = oldid;
				await this.save(list, `Updated [[${list[idx].pagename}]] (${old_oldid} -> ${oldid})`);
			},
			async remove(idx) {
				const list = await this.load();
				const removed = list.splice(idx, 1)[0];
				await this.save(list, `Removed [[${removed.pagename}]]`);
				return removed;
			},
			async set_status(idx, status) {
				const list = await this.load();
				list[idx].status = status;
				const action = status === "enabled" ? "Enabled" : "Disabled";
				await this.save(list, `${action} [[${list[idx].pagename}]]`);
			}
		};
		//#endregion
		//#region src/App.vue
		var _hoisted_1 = {
			key: 0,
			class: "smgr-header-nav"
		};
		var _hoisted_2 = {
			key: 1,
			class: "smgr-header-text"
		};
		var _hoisted_3 = { class: "smgr-header-actions" };
		var _hoisted_4 = { class: "smgr-body" };
		var _hoisted_5 = { key: 1 };
		var _hoisted_6 = { key: 2 };
		var _hoisted_7 = ["href"];
		var _hoisted_8 = { key: 0 };
		var _hoisted_9 = { key: 1 };
		var _hoisted_10 = { style: {
			"display": "flex",
			"gap": "8px",
			"justify-content": "flex-end"
		} };
		var App_default = /* @__PURE__ */ _plugin_vue_export_helper_default({
			__name: "App",
			setup(__props, { expose: __expose }) {
				const is_open = (0, vue.ref)(false);
				const all_disabled = (0, vue.ref)(localStorage.getItem("scriptmanager_disabled") === "true");
				const toggle_all_disabled = () => {
					all_disabled.value = !all_disabled.value;
					localStorage.setItem("scriptmanager_disabled", all_disabled.value ? "true" : "false");
					if (all_disabled.value) set_notice("All scripts disabled (preference saved locally).", "warning");
					else set_notice("Scripts enabled (preference saved locally).", "success");
				};
				const notice = (0, vue.ref)({
					text: "",
					type: "notice"
				});
				const busy_install = (0, vue.ref)(false);
				const show_warning = (0, vue.ref)(false);
				const pending_action = (0, vue.ref)({
					type: "install",
					name: "",
					idx: null,
					oldid: null
				});
				const busy_idx = (0, vue.ref)(null);
				const loading = (0, vue.ref)(false);
				const scripts = (0, vue.ref)([]);
				const updates = (0, vue.ref)({});
				const timestamps = (0, vue.ref)({});
				const current_view = (0, vue.ref)("list");
				const install_form_ref = (0, vue.ref)(null);
				const open = () => {
					notice.value = {
						text: "",
						type: "notice"
					};
					is_open.value = true;
					current_view.value = "list";
					if (install_form_ref.value) install_form_ref.value.clear_input();
					render_list();
				};
				const open_install = async (pagename) => {
					notice.value = {
						text: "",
						type: "notice"
					};
					is_open.value = true;
					current_view.value = "install";
					if (pagename) {
						on_install_requested(pagename);
						await (0, vue.nextTick)();
						if (install_form_ref.value) install_form_ref.value.set_input(pagename);
					}
					render_list();
				};
				__expose({
					open,
					open_install
				});
				const get_url = (pagename) => {
					return mw.util.getUrl(pagename);
				};
				const on_dialog_update = (val) => {
					is_open.value = val;
				};
				const set_notice = (text, type) => {
					notice.value = {
						text,
						type
					};
				};
				const clear_notice = () => {
					set_notice("");
				};
				const check_updates_and_timestamps = async (all_scripts) => {
					const current_scripts = all_scripts.filter((s) => s.status === "enabled");
					if (!current_scripts.length) return;
					const new_updates = {};
					const new_timestamps = {};
					const oldids = current_scripts.map((s) => s.oldid).filter((id) => id);
					if (oldids.length > 0) for (let i = 0; i < oldids.length; i += 50) {
						const chunk = oldids.slice(i, i + 50);
						try {
							const data2 = await new mw.Api().get({
								action: "query",
								prop: "revisions",
								revids: chunk.join("|"),
								rvprop: "timestamp|ids",
								formatversion: 2
							});
							for (const page of data2.query.pages || []) for (const rev of page.revisions || []) {
								const ts = new Date(rev.timestamp).toLocaleDateString(void 0, {
									year: "numeric",
									month: "short",
									day: "numeric"
								});
								const script = current_scripts.find((s) => s.oldid === rev.revid);
								if (script) new_timestamps[script.pagename] = ts;
							}
						} catch (e) {}
					}
					const pagenames = current_scripts.map((s) => s.pagename);
					try {
						const latest_map = await fetch_latest_revisions(pagenames);
						for (const s of current_scripts) {
							const latest = latest_map[s.pagename];
							if (latest && latest !== s.oldid) new_updates[s.pagename] = true;
						}
					} catch (e) {
						console.error("Failed to check for updates", e);
					}
					updates.value = new_updates;
					timestamps.value = new_timestamps;
				};
				const render_list = async () => {
					loading.value = true;
					scripts.value = await script_store.load();
					loading.value = false;
					check_updates_and_timestamps(scripts.value);
				};
				const on_install_requested = async (pagename_input) => {
					const pagename = pagename_input.trim();
					if (!pagename) return set_notice("Enter a page name.", "error");
					busy_install.value = true;
					clear_notice();
					try {
						const info = await fetch_page_info(pagename);
						if (!info.exists) set_notice(`Page not found: ${pagename}`, "error");
						else if (info.content_model !== "javascript") set_notice(`Script must have javascript content model (got: ${info.content_model})`, "error");
						else {
							if ((await script_store.load()).some((s) => s.pagename === info.normalized_title)) return set_notice(`${info.normalized_title} is already installed.`, "warning");
							pending_action.value = {
								type: "install",
								name: info.normalized_title,
								oldid: info.oldid,
								idx: null
							};
							show_warning.value = true;
						}
					} catch {
						set_notice(`Failed to fetch script info for ${pagename}.`, "error");
					} finally {
						busy_install.value = false;
					}
				};
				const confirm_action = async () => {
					if (pending_action.value.type === "install") await perform_install();
					else await perform_update();
				};
				const perform_install = async () => {
					const pagename = pending_action.value.name;
					const oldid = pending_action.value.oldid;
					show_warning.value = false;
					busy_install.value = true;
					clear_notice();
					try {
						await load_script(pagename, oldid);
						await script_store.add(pagename, oldid);
						if (install_form_ref.value) install_form_ref.value.clear_input();
						set_notice(`Installed ${pagename} (oldid ${oldid}).`, "success");
						current_view.value = "list";
						await render_list();
					} catch {
						set_notice(`Failed to install ${pagename}.`, "error");
					} finally {
						busy_install.value = false;
					}
				};
				const on_update = async (idx) => {
					pending_action.value = {
						type: "update",
						name: (await script_store.load())[idx].pagename,
						idx
					};
					show_warning.value = true;
				};
				const perform_update = async () => {
					const idx = pending_action.value.idx;
					show_warning.value = false;
					const script = (await script_store.load())[idx];
					busy_idx.value = idx;
					clear_notice();
					try {
						const info = await fetch_page_info(script.pagename);
						if (!info.exists) set_notice(`Page not found: ${script.pagename}`, "error");
						else if (info.oldid === script.oldid) {
							set_notice(`${script.pagename} is already up to date.`, "notice");
							await render_list();
						} else {
							await load_script(script.pagename, info.oldid);
							await script_store.update(idx, info.oldid);
							set_notice(`Updated ${script.pagename} to the latest version (oldid ${info.oldid}).`, "success");
							await render_list();
						}
					} catch {
						set_notice(`Failed to update ${script.pagename}.`, "error");
					} finally {
						busy_idx.value = null;
					}
				};
				const on_toggle = async (idx, current_status) => {
					const new_status = current_status === "enabled" ? "disabled" : "enabled";
					busy_idx.value = idx;
					clear_notice();
					try {
						await script_store.set_status(idx, new_status);
						await render_list();
					} catch {
						set_notice("Failed to update status.", "error");
					} finally {
						busy_idx.value = null;
					}
				};
				const on_uninstall = async (idx) => {
					busy_idx.value = idx;
					clear_notice();
					try {
						set_notice(`Removed ${(await script_store.remove(idx)).pagename}.`, "warning");
						await render_list();
					} catch {
						set_notice("Failed to remove script.", "error");
					} finally {
						busy_idx.value = null;
					}
				};
				return (_ctx, _cache) => {
					return (0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, null, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxDialog), {
						open: is_open.value,
						"onUpdate:open": [_cache[6] || (_cache[6] = ($event) => is_open.value = $event), on_dialog_update],
						class: "smgr-dialog",
						"close-button-label": "Close",
						"use-close-button": true
					}, {
						header: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["smgr-header-content", { "smgr-header-content--install": current_view.value === "install" }]) }, [current_view.value === "install" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), {
							onClick: _cache[0] || (_cache[0] = ($event) => current_view.value = "list"),
							action: "progressive",
							weight: "quiet",
							class: "smgr-back-btn"
						}, {
							default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(C7) }, null, 8, ["icon"]), _cache[10] || (_cache[10] = (0, vue.createTextVNode)("Back ", -1))]),
							_: 1
						}), _cache[11] || (_cache[11] = (0, vue.createElementVNode)("div", { class: "smgr-header-text" }, [(0, vue.createElementVNode)("span", { class: "cdx-dialog__header__title" }, "Install New Script")], -1))])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2, [..._cache[12] || (_cache[12] = [(0, vue.createElementVNode)("span", { class: "cdx-dialog__header__title" }, "script-ctl", -1), (0, vue.createElementVNode)("span", { class: "cdx-dialog__header__subtitle" }, "A security-focused user script manager", -1)])])), (0, vue.createElementVNode)("div", _hoisted_3, [
							current_view.value === "list" ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_wikimedia_codex.CdxButton), {
								key: 0,
								action: all_disabled.value ? "progressive" : "destructive",
								weight: "quiet",
								onClick: toggle_all_disabled
							}, {
								default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: all_disabled.value ? (0, vue.unref)(n4) : (0, vue.unref)(Y3) }, null, 8, ["icon"]), (0, vue.createTextVNode)((0, vue.toDisplayString)(all_disabled.value ? "Enable all scripts" : "Disable all scripts"), 1)]),
								_: 1
							}, 8, ["action"])) : (0, vue.createCommentVNode)("", true),
							current_view.value === "list" ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_wikimedia_codex.CdxButton), {
								key: 1,
								action: "progressive",
								weight: "primary",
								onClick: _cache[1] || (_cache[1] = ($event) => current_view.value = "install")
							}, {
								default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(C3) }, null, 8, ["icon"]), _cache[13] || (_cache[13] = (0, vue.createTextVNode)("Add script ", -1))]),
								_: 1
							})) : (0, vue.createCommentVNode)("", true),
							(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), {
								weight: "quiet",
								onClick: _cache[2] || (_cache[2] = ($event) => is_open.value = false),
								"aria-label": "Close"
							}, {
								default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxIcon), { icon: (0, vue.unref)(r4) }, null, 8, ["icon"]), _cache[14] || (_cache[14] = (0, vue.createTextVNode)("Close ", -1))]),
								_: 1
							})
						])], 2)]),
						default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_4, [notice.value.text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
							key: 0,
							class: (0, vue.normalizeClass)(["smgr-notice", `smgr-notice--${notice.value.type}`])
						}, (0, vue.toDisplayString)(notice.value.text), 3)) : (0, vue.createCommentVNode)("", true), current_view.value === "install" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5, [(0, vue.createVNode)(InstallForm_default, {
							ref_key: "install_form_ref",
							ref: install_form_ref,
							busy: busy_install.value,
							onInstall: on_install_requested,
							onCancel: _cache[3] || (_cache[3] = ($event) => current_view.value = "list")
						}, null, 8, ["busy"])])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6, [(0, vue.createVNode)(ScriptList_default, {
							scripts: scripts.value,
							updates: updates.value,
							timestamps: timestamps.value,
							loading: loading.value,
							busy_idx: busy_idx.value,
							all_disabled: all_disabled.value,
							onToggle: _cache[4] || (_cache[4] = ($event) => on_toggle($event.idx, $event.current_status)),
							onUpdate: on_update,
							onUninstall: on_uninstall,
							onAdd_new: _cache[5] || (_cache[5] = ($event) => current_view.value = "install")
						}, null, 8, [
							"scripts",
							"updates",
							"timestamps",
							"loading",
							"busy_idx",
							"all_disabled"
						])]))])]),
						_: 1
					}, 8, ["open"]), (0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxDialog), {
						open: show_warning.value,
						"onUpdate:open": [_cache[8] || (_cache[8] = ($event) => show_warning.value = $event), _cache[9] || (_cache[9] = ($event) => show_warning.value = $event)],
						title: "Security Warning",
						"close-button-label": "Cancel",
						"use-close-button": true
					}, {
						footer: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_10, [(0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), { onClick: _cache[7] || (_cache[7] = ($event) => show_warning.value = false) }, {
							default: (0, vue.withCtx)(() => [..._cache[21] || (_cache[21] = [(0, vue.createTextVNode)("Cancel", -1)])]),
							_: 1
						}), (0, vue.createVNode)((0, vue.unref)(_wikimedia_codex.CdxButton), {
							action: "destructive",
							weight: "primary",
							onClick: confirm_action,
							disabled: busy_install.value
						}, {
							default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(pending_action.value.type === "install" ? "I Understand, Install" : "I Understand, Update"), 1)]),
							_: 1
						}, 8, ["disabled"])])]),
						default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("p", null, [
							_cache[15] || (_cache[15] = (0, vue.createElementVNode)("strong", null, "WARNING:", -1)),
							_cache[16] || (_cache[16] = (0, vue.createTextVNode)(" Userscripts could contain malicious content capable of compromising your account. You are expected to review the source code of the script before installing it: ", -1)),
							(0, vue.createElementVNode)("a", {
								href: get_url(pending_action.value.name),
								target: "_blank"
							}, [(0, vue.createElementVNode)("strong", null, (0, vue.toDisplayString)(pending_action.value.name), 1)], 8, _hoisted_7)
						]), pending_action.value.type === "install" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_8, [
							_cache[17] || (_cache[17] = (0, vue.createTextVNode)(" Are you absolutely sure you want to install and trust ", -1)),
							(0, vue.createElementVNode)("code", null, (0, vue.toDisplayString)(pending_action.value.name), 1),
							_cache[18] || (_cache[18] = (0, vue.createTextVNode)("? ", -1))
						])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_9, [
							_cache[19] || (_cache[19] = (0, vue.createTextVNode)(" Are you absolutely sure you want to update and trust the new version of ", -1)),
							(0, vue.createElementVNode)("code", null, (0, vue.toDisplayString)(pending_action.value.name), 1),
							_cache[20] || (_cache[20] = (0, vue.createTextVNode)("? ", -1))
						]))]),
						_: 1
					}, 8, ["open"])], 64);
				};
			}
		}, [["__scopeId", "data-v-5c4ff0aa"]]);
		//#endregion
		//#region src/init.js
		async function init() {
			const is_disabled = localStorage.getItem("scriptmanager_disabled") === "true";
			const scripts = await script_store.load();
			if (!is_disabled) scripts.filter((s) => s.status === "enabled").forEach((s) => load_script(s.pagename, s.oldid));
			let app_instance = null;
			const ensure_app_instance = () => {
				if (!app_instance) {
					const app_mount = document.createElement("div");
					app_mount.id = "script-manager-vue-root";
					document.body.appendChild(app_mount);
					app_instance = (0, vue.createApp)(App_default).mount(app_mount);
				}
				return app_instance;
			};
			const link = mw.util.addPortletLink("p-personal", "#", "Script Manager", "pt-scriptmanager", "Install and manage userscripts");
			if (link) link.addEventListener("click", (e) => {
				e.preventDefault();
				const inst = ensure_app_instance();
				if (inst && typeof inst.open === "function") inst.open();
			});
			if (mw.config.get("wgPageName") === "Wikipedia:User_scripts/List") document.querySelectorAll("span.scriptInstallerLink").forEach((span) => {
				const id = span.id;
				if (id && id.endsWith(".js")) {
					const install_link = document.createElement("a");
					install_link.textContent = " [install]";
					install_link.href = "#";
					install_link.style.cursor = "pointer";
					install_link.className = "script-ctl-install-link";
					install_link.addEventListener("click", (e) => {
						e.preventDefault();
						const inst = ensure_app_instance();
						if (inst && typeof inst.open_install === "function") inst.open_install(id);
					});
					span.after(install_link);
				}
			});
		}
		init();
		//#endregion
	})(require("vue"), require("@wikimedia/codex"));
});
(function() {
	//#region \0vite/all-css
	try {
		if (typeof document != "undefined") {
			var elementStyle = document.createElement("style");
			elementStyle.appendChild(document.createTextNode(".smgr-install-view[data-v-05e069a8] {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n.smgr-card[data-v-05e069a8] {\n  background: var(--background-color-base, #fff);\n  border: 1px solid var(--border-color-subtle, #c8ccd1);\n  border-radius: 4px;\n  padding: 16px;\n}\n.smgr-install-card[data-v-05e069a8] {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n}\n.smgr-label[data-v-05e069a8] {\n  font-weight: bold;\n  font-size: 0.9em;\n}\n.smgr-install-row[data-v-05e069a8] {\n  display: flex;\n  gap: 10px;\n}\n.smgr-input[data-v-05e069a8] {\n  flex: 1;\n}\n.smgr-browse-footer[data-v-05e069a8] {\n  margin-top: 4px;\n}\n\n.smgr-state[data-v-e878156a] {\r\n  padding: 24px 0;\r\n  text-align: center;\r\n  color: var(--color-placeholder, #72777d);\n}\n.smgr-script-cell[data-v-e878156a] {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\r\n  margin-left: -12px;\n}\n.smgr-dimmed[data-v-e878156a] {\r\n  opacity: 0.45;\n}\n.smgr-pagename[data-v-e878156a] {\r\n  font-weight: 600;\r\n  color: var(--color-progressive, #36c);\r\n  text-decoration: none;\n}\n.smgr-pagename[data-v-e878156a]:hover {\r\n  text-decoration: underline;\n}\n.smgr-update-chip[data-v-e878156a] {\r\n  display: inline-flex;\r\n  align-items: center;\r\n  gap: 3px;\r\n  font-size: 0.72em;\r\n  font-weight: 600;\r\n  color: var(--color-progressive, #36c);\r\n  background: var(--background-color-progressive-subtle, #eaf3ff);\r\n  padding: 1px 6px;\r\n  border-radius: 3px;\n}\n.smgr-update-chip .cdx-icon[data-v-e878156a] {\r\n  width: 12px;\r\n  height: 12px;\n}\n.smgr-empty[data-v-e878156a] {\r\n  color: var(--color-placeholder, #72777d);\n}\r\n\n.smgr-body[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 20px;\n}\n.smgr-header-content[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  width: 100%;\n}\n.smgr-header-nav[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\r\n  margin-left: -8px;\n}\n.smgr-header-text[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  flex-direction: column;\n}\n.smgr-header-actions[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\n}\n.smgr-header-content--install .cdx-dialog__header__title[data-v-5c4ff0aa] {\r\n  font-size: 1rem;\n}\n.smgr-notice[data-v-5c4ff0aa] {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\r\n  padding: 10px 14px;\r\n  border-radius: 4px;\r\n  border-left: 4px solid;\r\n  font-size: 0.9rem;\r\n  font-weight: 500;\n}\n.smgr-notice--success[data-v-5c4ff0aa] {\r\n  background: var(--background-color-success-subtle, #d5fdf4);\r\n  border-color: var(--border-color-success, #096450);\r\n  color: var(--color-success, #14866d);\n}\n.smgr-notice--error[data-v-5c4ff0aa] {\r\n  background: var(--background-color-error-subtle, #fee7e6);\r\n  border-color: var(--border-color-error, #b32424);\r\n  color: var(--color-error, #d73333);\n}\n.smgr-notice--warning[data-v-5c4ff0aa] {\r\n  background: var(--background-color-warning-subtle, #fef6e7);\r\n  border-color: var(--border-color-warning, #a66200);\r\n  color: var(--border-color-warning, #a66200);\n}\n.smgr-notice--notice[data-v-5c4ff0aa] {\r\n  background: var(--background-color-notice-subtle, #eaecf0);\r\n  border-color: var(--border-color-notice, #54595d);\r\n  color: var(--color-notice, #202122);\n}\r\n\n.smgr-dialog.cdx-dialog__window,\r\n.smgr-dialog .cdx-dialog__window,\r\n.smgr-dialog {\r\n  width: 800px !important;\r\n  max-width: 90vw !important;\n}\r\n/*$vite$:1*/"));
			document.head.appendChild(elementStyle);
		}
	} catch (e) {
		console.error("vite-plugin-css-injected-by-js", e);
	}
	//#endregion
})();

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.