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)