Electron built-in spell checker

This commit is contained in:
Mr. Stallion 2020-04-02 19:46:36 -05:00
parent 165007d1c1
commit a599e17e1b
7 changed files with 213 additions and 81 deletions

View File

@ -35,6 +35,7 @@
<script lang="ts">
import Sortable = require('sortablejs'); //tslint:disable-line:no-require-imports
import * as _ from 'lodash';
import {Component, Hook} from '@f-list/vue-ts';
import * as electron from 'electron';
@ -44,6 +45,7 @@
import Vue from 'vue';
import l from '../chat/localize';
import {GeneralSettings} from './common';
import { getSafeLanguages } from './language';
const browserWindow = electron.remote.getCurrentWindow();
@ -87,15 +89,26 @@
// top bar devtools
// browserWindow.webContents.openDevTools({ mode: 'detach' });
browserWindow.webContents.session.setSpellCheckerLanguages(getSafeLanguages(this.settings.spellcheckLang));
await this.addTab();
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
electron.ipcRenderer.on('settings', (_e: Event, settings: GeneralSettings) => this.settings = settings);
electron.ipcRenderer.on('allow-new-tabs', (_e: Event, allow: boolean) => this.canOpenTab = allow);
electron.ipcRenderer.on('open-tab', () => this.addTab());
electron.ipcRenderer.on('update-available', (_: Event, available: boolean) => this.hasUpdate = available);
electron.ipcRenderer.on('update-available', (_e: Event, available: boolean) => this.hasUpdate = available);
electron.ipcRenderer.on('fix-logs', () => this.activeTab!.view.webContents.send('fix-logs'));
electron.ipcRenderer.on('quit', () => this.destroyAllTabs());
electron.ipcRenderer.on('connect', (_: Event, id: number, name: string) => {
electron.ipcRenderer.on('update-dictionaries', (_e: Event, langs: string[]) => {
browserWindow.webContents.session.setSpellCheckerLanguages(langs);
for (const t of this.tabs) {
t.view.webContents.session.setSpellCheckerLanguages(langs);
}
});
electron.ipcRenderer.on('connect', (_e: Event, id: number, name: string) => {
const tab = this.tabMap[id];
tab.user = name;
tab.tray.setToolTip(`${l('title')} - ${tab.user}`);
@ -103,7 +116,7 @@
menu.unshift({label: tab.user, enabled: false}, {type: 'separator'});
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(menu));
});
electron.ipcRenderer.on('disconnect', (_: Event, id: number) => {
electron.ipcRenderer.on('disconnect', (_e: Event, id: number) => {
const tab = this.tabMap[id];
if(tab.hasNew) {
tab.hasNew = false;
@ -113,18 +126,18 @@
tab.tray.setToolTip(l('title'));
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
});
electron.ipcRenderer.on('has-new', (_: Event, id: number, hasNew: boolean) => {
electron.ipcRenderer.on('has-new', (_e: Event, id: number, hasNew: boolean) => {
const tab = this.tabMap[id];
tab.hasNew = hasNew;
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
});
browserWindow.on('maximize', () => this.isMaximized = true);
browserWindow.on('unmaximize', () => this.isMaximized = false);
electron.ipcRenderer.on('switch-tab', (_: Event) => {
electron.ipcRenderer.on('switch-tab', (_e: Event) => {
const index = this.tabs.indexOf(this.activeTab!);
this.show(this.tabs[index + 1 === this.tabs.length ? 0 : index + 1]);
});
electron.ipcRenderer.on('show-tab', (_: Event, id: number) => {
electron.ipcRenderer.on('show-tab', (_e: Event, id: number) => {
this.show(this.tabMap[id]);
});
document.addEventListener('click', () => this.activeTab!.view.webContents.focus());
@ -195,12 +208,14 @@
if(this.lockTab) return;
const tray = new electron.remote.Tray(trayIcon);
tray.setToolTip(l('title'));
tray.on('click', (_) => this.trayClicked(tab));
tray.on('click', (_e) => this.trayClicked(tab));
const view = new electron.remote.BrowserView({webPreferences: {webviewTag: true, nodeIntegration: true, spellcheck: true}});
// tab devtools
// view.webContents.openDevTools();
view.webContents.session.setSpellCheckerLanguages(getSafeLanguages(this.settings.spellcheckLang));
view.setAutoResize({width: true, height: true});
electron.ipcRenderer.send('tab-added', view.webContents.id);
const tab = {active: false, view, user: undefined, hasNew: false, tray};

View File

@ -161,17 +161,17 @@ webContents.on('context-menu', (_, props) => {
click: () => electron.clipboard.writeText(props.selectionText)
});
if(props.misspelledWord !== '') {
// const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
// menuTemplate.unshift({
// label: l('spellchecker.add'),
// click: () => electron.ipcRenderer.send('dictionary-add', props.misspelledWord)
// }, {type: 'separator'});
// if(corrections.length > 0)
// menuTemplate.unshift(...corrections.map((correction: string) => ({
// label: correction,
// click: () => webContents.replaceMisspelling(correction)
// })));
// else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')});
const corrections = props.dictionarySuggestions; //spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
menuTemplate.unshift({
label: l('spellchecker.add'),
click: () => electron.ipcRenderer.send('dictionary-add', props.misspelledWord)
}, {type: 'separator'});
if(corrections.length > 0)
menuTemplate.unshift(...corrections.map((correction: string) => ({
label: correction,
click: () => webContents.replaceMisspelling(correction)
})));
else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')});
} else if(settings.customDictionary.indexOf(props.selectionText) !== -1)
menuTemplate.unshift({
label: l('spellchecker.remove'),

View File

@ -9,7 +9,7 @@ export class GeneralSettings {
profileViewer = true;
host = defaultHost;
logDirectory = path.join(electron.app.getPath('userData'), 'data');
spellcheckLang: string | undefined = 'en_GB';
spellcheckLang: string[] | string | undefined = 'en_GB';
theme = 'default';
version = electron.app.getVersion();
beta = false;
@ -24,4 +24,4 @@ export function nativeRequire<T>(module: string): T {
return Module.prototype.require.call({paths: Module._nodeModulePaths(__dirname)}, module);
}
//tslint:enable
//tslint:enable

View File

@ -4,6 +4,7 @@ import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import * as fs from 'fs';
import * as path from 'path';
import {promisify} from 'util';
import * as _ from 'lodash';
const dictDir = path.join(electron.app.getPath('userData'), 'spellchecker');
fs.mkdirSync(dictDir, {recursive: true});
@ -46,4 +47,4 @@ export async function ensureDictionary(lang: string): Promise<void> {
}
await ensure('aff');
await ensure('dic');
}
}

67
electron/language.ts Normal file
View File

@ -0,0 +1,67 @@
import * as _ from 'lodash';
export function getSafeLanguages(langs: string | string[] | undefined): string[] {
return langs ? _.castArray(langs) : [];
}
export const knownLanguageNames = {
af: 'Afrikaans',
bg: 'Bulgarian',
ca: 'Catalan',
cs: 'Czech',
cy: 'Welsh',
da: 'Danish',
de: 'German',
el: 'Greek',
'en-AU': 'English, Australian',
'en-CA': 'English, Canadian',
'en-GB': 'English, British',
'en-US': 'English, American',
es: 'Spanish',
'es-419': 'Spanish, Latin America and Caribbean',
'es-AR': 'Spanish, Argentine',
'es-ES': 'Spanish, Castilian',
'es-MX': 'Spanish, Mexican',
'es-US': 'Spanish, American',
et: 'Estonian',
fa: 'Persian',
fi: 'Finnish',
fo: 'Faroese',
fr: 'French',
he: 'Hebrew',
hi: 'Hindi',
hr: 'Croatian',
hu: 'Hungarian',
hy: 'Armenian',
id: 'Indonesian',
it: 'Italian',
ko: 'Korean',
lt: 'Lithuanian',
lv: 'Latvian',
nb: 'Norwegian',
nl: 'Dutch',
pl: 'Polish',
'pt-BR': 'Portuguese, Brazilian',
'pt-PT': 'Portuguese, European',
ro: 'Romanian',
ru: 'Russian',
sh: 'Serbo-Croatian',
sk: 'Slovak',
sl: 'Slovenian',
sq: 'Albanian',
sr: 'Serbian',
sv: 'Swedish',
ta: 'Tamil',
tg: 'Tajik',
tr: 'Turkish',
uk: 'Ukranian',
vi: 'Vietnamese'
};

View File

@ -41,13 +41,14 @@ import * as path from 'path';
// import * as url from 'url';
import l from '../chat/localize';
import {defaultHost, GeneralSettings} from './common';
import {ensureDictionary, getAvailableDictionaries} from './dictionaries';
import { getSafeLanguages, knownLanguageNames } from './language';
import * as windowState from './window_state';
import BrowserWindow = Electron.BrowserWindow;
import MenuItem = Electron.MenuItem;
import { ElectronBlocker } from '@cliqz/adblocker-electron';
import fetch from 'node-fetch';
import MenuItemConstructorOptions = Electron.MenuItemConstructorOptions;
import * as _ from 'lodash';
// Module to control application life.
const app = electron.app;
@ -80,10 +81,46 @@ if(!settings.hwAcceleration) {
app.disableHardwareAcceleration();
}
async function setDictionary(lang: string | undefined): Promise<void> {
if(lang !== undefined) await ensureDictionary(lang);
settings.spellcheckLang = lang;
// async function setDictionary(lang: string | undefined): Promise<void> {
// if(lang !== undefined) await ensureDictionary(lang);
// settings.spellcheckLang = lang;
// setGeneralSettings(settings);
// }
export function updateSpellCheckerLanguages(langs: string[]): void {
// console.log('Language support:', langs);
for (const w of windows) {
// console.log('LANG SEND');
w.webContents.send('update-dictionaries', langs);
}
electron.session.defaultSession.setSpellCheckerLanguages(langs);
}
async function toggleDictionary(lang: string): Promise<void> {
const activeLangs = getSafeLanguages(settings.spellcheckLang);
// console.log('INITIAL LANG', activeLangs, lang);
let newLangs: string[] = [];
if (_.indexOf(activeLangs, lang) >= 0) {
newLangs = _.reject(activeLangs, (al) => (al === lang));
} else {
activeLangs.push(lang);
newLangs = activeLangs;
}
settings.spellcheckLang = newLangs;
setGeneralSettings(settings);
// console.log('NEW LANG', newLangs);
updateSpellCheckerLanguages(newLangs);
}
function setGeneralSettings(value: GeneralSettings): void {
@ -93,20 +130,23 @@ function setGeneralSettings(value: GeneralSettings): void {
}
async function addSpellcheckerItems(menu: Electron.Menu): Promise<void> {
if(settings.spellcheckLang !== undefined) await ensureDictionary(settings.spellcheckLang);
const dictionaries = await getAvailableDictionaries();
const selected = settings.spellcheckLang;
menu.append(new electron.MenuItem({
type: 'radio',
label: l('settings.spellcheck.disabled'),
click: async() => setDictionary(undefined)
}));
for(const lang of dictionaries)
const selected = getSafeLanguages(settings.spellcheckLang);
const langs = electron.session.defaultSession.availableSpellCheckerLanguages;
const sortedLangs = _.sortBy(
_.map(
langs,
(lang) => ({lang, name: (lang in knownLanguageNames) ? `${(knownLanguageNames as any)[lang]} (${lang})` : lang})
),
'name'
);
for (const lang of sortedLangs)
menu.append(new electron.MenuItem({
type: 'radio',
label: lang,
checked: lang === selected,
click: async() => setDictionary(lang)
type: 'checkbox',
label: lang.name,
checked: (_.indexOf(selected, lang.lang) >= 0),
click: async() => toggleDictionary(lang.lang)
}));
}
@ -140,6 +180,9 @@ function createWindow(): Electron.BrowserWindow | undefined {
const window = new electron.BrowserWindow(windowProperties);
windows.push(window);
const safeLanguages = settings.spellcheckLang ? _.castArray(settings.spellcheckLang) : [];
electron.session.defaultSession.setSpellCheckerLanguages(safeLanguages);
// tslint:disable-next-line:no-floating-promises
ElectronBlocker.fromLists(
fetch,
@ -160,31 +203,31 @@ function createWindow(): Electron.BrowserWindow | undefined {
(blocker) => {
blocker.enableBlockingInSession(electron.session.defaultSession);
// console.log('Got this far!!!!');
blocker.on('request-blocked', (request: Request) => {
console.log('blocked', request.url);
});
blocker.on('request-redirected', (request: Request) => {
console.log('redirected', request.url);
});
blocker.on('request-whitelisted', (request: Request) => {
console.log('whitelisted', request.url);
});
blocker.on('csp-injected', (request: Request) => {
console.log('csp', request.url);
});
blocker.on('script-injected', (script: string, url: string) => {
console.log('script', script.length, url);
});
blocker.on('style-injected', (style: string, url: string) => {
console.log('style', style.length, url);
});
// // console.log('Got this far!!!!');
//
// blocker.on('request-blocked', (request: Request) => {
// console.log('blocked', request.url);
// });
//
// blocker.on('request-redirected', (request: Request) => {
// console.log('redirected', request.url);
// });
//
// blocker.on('request-whitelisted', (request: Request) => {
// console.log('whitelisted', request.url);
// });
//
// blocker.on('csp-injected', (request: Request) => {
// console.log('csp', request.url);
// });
//
// blocker.on('script-injected', (script: string, url: string) => {
// console.log('script', script.length, url);
// });
//
// blocker.on('style-injected', (style: string, url: string) => {
// console.log('style', style.length, url);
// });
}
);
@ -302,14 +345,14 @@ function onReady(): void {
{label: l('action.newWindow'), click: createWindow, accelerator: 'CmdOrCtrl+n'},
{
label: l('action.newTab'),
click: (_: Electron.MenuItem, w: Electron.BrowserWindow) => {
click: (_m: Electron.MenuItem, w: Electron.BrowserWindow) => {
if(tabCount < 3) w.webContents.send('open-tab');
},
accelerator: 'CmdOrCtrl+t'
},
{
label: l('settings.logDir'),
click: (_, window: BrowserWindow) => {
click: (_m, window: BrowserWindow) => {
const dir = electron.dialog.showOpenDialogSync(
{defaultPath: settings.logDirectory, properties: ['openDirectory']});
if(dir !== undefined) {
@ -367,14 +410,14 @@ function onReady(): void {
}
}, {
label: l('fixLogs.action'),
click: (_, window: BrowserWindow) => window.webContents.send('fix-logs')
click: (_m, window: BrowserWindow) => window.webContents.send('fix-logs')
},
{type: 'separator'},
{role: 'minimize'},
{
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : undefined,
label: l('action.quit'),
click(_: Electron.MenuItem, window: Electron.BrowserWindow): void {
click(_m: Electron.MenuItem, window: Electron.BrowserWindow): void {
if(characters.length === 0) return app.quit();
const button = electron.dialog.showMessageBoxSync(window, {
message: l('chat.confirmLeave'),
@ -426,7 +469,7 @@ function onReady(): void {
]
}
]));
electron.ipcMain.on('tab-added', (_: Event, id: number) => {
electron.ipcMain.on('tab-added', (_event: Event, id: number) => {
const webContents = electron.webContents.fromId(id);
setUpWebContents(webContents);
++tabCount;
@ -437,7 +480,7 @@ function onReady(): void {
--tabCount;
for(const w of windows) w.webContents.send('allow-new-tabs', true);
});
electron.ipcMain.on('save-login', (_: Event, account: string, host: string) => {
electron.ipcMain.on('save-login', (_event: Event, account: string, host: string) => {
settings.account = account;
settings.host = host;
setGeneralSettings(settings);
@ -447,16 +490,17 @@ function onReady(): void {
characters.push(character);
e.returnValue = true;
});
electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
if(settings.customDictionary.indexOf(word) !== -1) return;
settings.customDictionary.push(word);
setGeneralSettings(settings);
electron.ipcMain.on('dictionary-add', (_event: Event, word: string) => {
// if(settings.customDictionary.indexOf(word) !== -1) return;
// settings.customDictionary.push(word);
// setGeneralSettings(settings);
for(const w of windows) w.webContents.session.addWordToSpellCheckerDictionary(word);
});
electron.ipcMain.on('dictionary-remove', (_: Event, word: string) => {
settings.customDictionary.splice(settings.customDictionary.indexOf(word), 1);
setGeneralSettings(settings);
electron.ipcMain.on('dictionary-remove', (_event: Event /*, word: string*/) => {
// settings.customDictionary.splice(settings.customDictionary.indexOf(word), 1);
// setGeneralSettings(settings);
});
electron.ipcMain.on('disconnect', (_: Event, character: string) => {
electron.ipcMain.on('disconnect', (_event: Event, character: string) => {
const index = characters.indexOf(character);
if(index !== -1) characters.splice(index, 1);
});

View File

@ -20,12 +20,13 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
* Ad auto-posting
* Manage channel ad settings via "Tab Settings"
* Automatically re-post ads every 11-18 minutes (randomized) for up to 180 minutes
* Rotate multiple ads on a single channel by entering multiple ads in "Tab Settings"
* Rotate multiple ads on a single channel by entering multiple ads in "Ad Settings"
* Ad ratings
* LFP ads are automatically rated and matched against your profile
* LFP ads are automatically rated (great/good/maybe/no) and matched against your profile
* Link previews
* Hover cursor over any `[url]` to see a preview of it
* Middle click any `[url]` to turn the preview into a sticky / interactive mode
* Link preview has an ad-blocker to minimize page load times and protect against unfriendly scripts
* Profile
* Kinks are auto-compared when viewing character profile
* Custom kink explanations can be expanded inline
@ -51,6 +52,10 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
* Conversation dialog can be opened by typing in a character name
* Message search matches character names
* PM list shows characters' online status as a colored icon
* Details for Nerds
* Upgraded to Electron 8.x
* Replaced node-spellchecker with the built-in spellchecker of Electron 8
* Multi-language support for spell checking (Windows only language support is fully automatic on MacOS)
## How to Set Up Ads