diff --git a/electron/Window.vue b/electron/Window.vue index c18f52d..9a28008 100644 --- a/electron/Window.vue +++ b/electron/Window.vue @@ -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}; diff --git a/electron/chat.ts b/electron/chat.ts index 6e1a65a..0d34286 100644 --- a/electron/chat.ts +++ b/electron/chat.ts @@ -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'), diff --git a/electron/common.ts b/electron/common.ts index 978082b..733754d 100644 --- a/electron/common.ts +++ b/electron/common.ts @@ -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 \ No newline at end of file +//tslint:enable diff --git a/electron/dictionaries.ts b/electron/dictionaries.ts index a309018..a2ccf10 100644 --- a/electron/dictionaries.ts +++ b/electron/dictionaries.ts @@ -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'); -} \ No newline at end of file +} diff --git a/electron/language.ts b/electron/language.ts new file mode 100644 index 0000000..546793d --- /dev/null +++ b/electron/language.ts @@ -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' +}; + diff --git a/electron/main.ts b/electron/main.ts index 76fe371..9278796 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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); }); diff --git a/readme.md b/readme.md index 06f5bec..9172e82 100644 --- a/readme.md +++ b/readme.md @@ -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