import {TEXT_CODING_MAP} from "./scr/charsets.js" import * as mv3 from "./scr/mv3_util.js" function allocateRuleId(oldRules){ function id_occupied(id){ for (let rule of oldRules){ if (rule.id === id){ return true } } return false } //for historical reason, oldRule IDs can be sparsely large, so we search empty slots instead of increment on old ID for (let i = 1; i < 32768; i++){ if (! id_occupied(i)){ //console.log(`allocate new rule id = ${i}`) return i } } return 1 //not likely to happen } async function getLiveCodeMap(){ let live_map = {...TEXT_CODING_MAP}; let items = await chrome.storage.sync.get({ "disabled_menu": "[]", "usr_menu": "[]" }) let disabled_menu = JSON.parse(items["disabled_menu"]); let usr_menu = JSON.parse(items["usr_menu"]); disabled_menu.forEach(function(code){ delete(live_map[code]); }) //default menu may be overwritten by user menu if code is the same usr_menu.forEach(function({lang, code}){ live_map[code] = [lang, code, true]; //3rd item as flag of user defined }) return live_map } //extract site url pattern from page url //this pattern serves as url filter for dynamic rules function extract_site_url_pattern(url) { if (typeof(url) !== "string"){ return "Newtab"; } var fragments = url.split('/').slice(0, 3); return fragments.join("/") + "/*"; } function injectTxt(txt){ function _escapeHTML(html) { return html.replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", ''') } let url = location.href let is_html = !!url.match(/\.html?$/i) let mime_marker = is_html ? "html" : "plain" let doc_contents = is_html ? txt : `
${_escapeHTML(txt)}
` let newDoc = document.open(`text/${mime_marker}`, "replace") newDoc.write(doc_contents) newDoc.close() } //load local file with specified character set async function loadLocalFile(url, new_code, tabId){ //XMLHttpRequest() not allowed from file:// scheme let response = null try{ response = await fetch(url) } catch (e){ chrome.tabs.create({ url: chrome.i18n.getMessage("msgPage")}); return } const buf = await response.arrayBuffer() let decoder = new TextDecoder(new_code); let txt = decoder.decode(buf) chrome.scripting.executeScript({ target: {tabId: tabId}, func: injectTxt, args: [txt], }); } // The onClicked callback function. async function onClickHandler(info, tab) { var new_code = info.menuItemId; if (new_code === "help"){ var locale = chrome.i18n.getMessage("_locale"); chrome.tabs.create({ url: "http://www.deep-watch.net/" + locale + "/blog/bring-character-encoding-menu-back-to-chrome-with-extension"} ); } let flg_need_reload = false; let flg_add_rule = true let url_pattern = extract_site_url_pattern(tab.url); let oldRules = await chrome.declarativeNetRequest.getDynamicRules() let id_to_remove = mv3.findRuleIdByUrl(oldRules, url_pattern) if (new_code == "default"){ flg_need_reload = true; flg_add_rule = false } else{ if (url_pattern == "file:///*"){ await loadLocalFile(tab.url, new_code, tab.id); } else{ flg_need_reload = true; } } if (flg_need_reload){ //if (flg_add_rule && id_to_remove){ // console.log(`reuse rule id = ${id_to_remove}`) //} //reuse old rule ID to avoid adding duplicated rules for the same site let new_id = id_to_remove || allocateRuleId(oldRules) await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds : id_to_remove ? [id_to_remove] : [], addRules : flg_add_rule ? [mv3.composeRule(new_id, url_pattern, new_code)] : [] }) chrome.tabs.update(tab.id, {url: tab.url}); } } chrome.contextMenus.onClicked.addListener(onClickHandler); //update selected coding every time new page loaded //this action may be expensive chrome.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) { if (changeInfo.status === "complete") { //update context menu const g_site2Code = await mv3.get_site2Code() var url_pattern = extract_site_url_pattern(tab.url); var my_code = g_site2Code[url_pattern] || "default"; let live_code_map = await getLiveCodeMap() //update menu status /* for (var menuId in live_code_map){ chrome.contextMenus.update(menuId, { "checked" : false }) } */ if (live_code_map[my_code]){ chrome.contextMenus.update(my_code, { "checked" : true }, ()=>{ let err = chrome.runtime.lastError if (err && err.message.indexOf("Cannot find menu item with id") >=0){ //for some reason, menu may not be present updateContextMenu().then(()=>{ chrome.contextMenus.update(my_code, { "checked" : true }) }) } }) } else{ //console.warn(`${my_code} not found in menu`) let oldRules = await chrome.declarativeNetRequest.getDynamicRules() let ruleId = await mv3.findRuleIdByUrl(oldRules, url_pattern) if (ruleId){ //console.log(`remove code ${my_code} for site ${url_pattern}`) await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds : [ruleId] }) } } } }); async function updateContextMenu(){ let live_code_map = await getLiveCodeMap() await chrome.contextMenus.removeAll() chrome.contextMenus.create({ "title": chrome.i18n.getMessage("Help_page"), "type" : "normal", "contexts":["page"], "id": "help" }); chrome.contextMenus.create({ "type" : "separator", "contexts":["page"], "id": "separator" }); for (var [code, [language, ui_code, is_custom]] of Object.entries(live_code_map)) { if (! is_custom){ language = chrome.i18n.getMessage(language) } var menu_title = language if (ui_code){ menu_title += " ("+ ui_code + ")"; } chrome.contextMenus.create({ "title": menu_title, "type" : "radio", "contexts":["page"], "id": code }); } console.log("Context menu updated") } chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === "sync"){ updateContextMenu().then(()=>{}) } }) // Set up context menu at install time. chrome.runtime.onInstalled.addListener(updateContextMenu)