252 lines
7.4 KiB
JavaScript
252 lines
7.4 KiB
JavaScript
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 : `<pre>${_escapeHTML(txt)}</pre>`
|
|
|
|
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) |