meta: change naming convention of interfaces and use importsNotUsedAsValues: 'error' in tsconfig for clearer imports

This commit is contained in:
Xymorot 2021-01-06 02:35:46 +01:00
parent a807c6f2be
commit 09dc14726d
83 changed files with 219 additions and 213 deletions

View File

@ -150,7 +150,7 @@
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
"suffix": ["Interface"]
}
],
"@typescript-eslint/explicit-member-accessibility": "error",

View File

@ -4,21 +4,20 @@ import './main/core/install';
import { app } from 'electron';
import { isDev } from './main/core/env';
import { IAppWindow } from './main/modules/app-window/i-app-window';
import { ILogger } from './main/modules/logger/i-logger';
import type { AppWindowInterface } from './main/modules/app-window/app-window-interface';
/**
* have a read: https://github.com/nodejs/node/issues/20392, over 100 comments as of 2020-07-26
* https://nodejs.org/api/process.html#process_event_unhandledrejection
*/
process.on('unhandledRejection', (reason) => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
void logger.fatal(`Unhandled Rejection, see ${logger.getExceptionsLogFile()}`);
throw reason;
});
process.on('uncaughtException', (error) => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
void logger.exception(error);
if (isDev()) {
// eslint-disable-next-line no-console -- only for development purposes
@ -27,7 +26,7 @@ process.on('uncaughtException', (error) => {
});
async function createWindow(): Promise<void> {
const appWindowMain: IAppWindow = container.get('app-window-main');
const appWindowMain: AppWindowInterface = container.get('app-window-main');
await appWindowMain.open();
appWindowMain.window?.on('closed', () => {
@ -48,7 +47,7 @@ app.on('window-all-closed', () => {
app.on('activate', async () => {
// On OS X it"s common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
const appWindowMain: IAppWindow = container.get('app-window-main');
const appWindowMain: AppWindowInterface = container.get('app-window-main');
if (appWindowMain.isClosed()) {
await createWindow();
}

View File

@ -1,6 +1,6 @@
import path from 'path';
import { Connection, createConnection as ormCreateConnection } from 'typeorm';
import { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions';
import type { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions';
import { appPath } from './app-path';
export enum Database {

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Author } from './author';
@Entity()
export class AuthorName implements IIdentifiableEntity, INameEntity {
export class AuthorName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AuthorRole } from './author-role';
@Entity()
export class AuthorRoleName implements IIdentifiableEntity, INameEntity {
export class AuthorRoleName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -7,7 +7,7 @@ import { WorkAuthor } from './work-author';
* Examples: story writing, drawing, animating, publishing, ...
*/
@Entity()
export class AuthorRole implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity {
export class AuthorRole implements IdentifiableEntityInterface, MultiNamedEntityInterface, DescribableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -6,7 +6,7 @@ import { WorkAuthor } from './work-author';
* This entity represents a single real-world entity, be it a person or named group of persons.
*/
@Entity()
export class Author implements IIdentifiableEntity, IMultiNamedEntity {
export class Author implements IdentifiableEntityInterface, MultiNamedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -8,7 +8,7 @@ import { WorkCharacter } from './work-character';
*/
@Entity()
@PercentCheck('weight')
export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
export class CharacterTag implements IdentifiableEntityInterface, WeightedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Collection } from './collection';
@Entity()
export class CollectionName implements IIdentifiableEntity, INameEntity {
export class CollectionName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -7,7 +7,7 @@ import { Work } from './work';
* The main use case is chronological ordering.
*/
@Entity()
export class CollectionPart implements IIdentifiableEntity, IOrderableEntity {
export class CollectionPart implements IdentifiableEntityInterface, OrderableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -11,7 +11,7 @@ import { CollectionPart } from './collection-part';
* If authors of works see them as belonging together, they are a collection
*/
@Entity()
export class Collection implements IIdentifiableEntity, IMultiNamedEntity {
export class Collection implements IdentifiableEntityInterface, MultiNamedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -9,7 +9,7 @@ import { Work } from './work';
* Multiple works can have multiple copies (think of different scans of a physical work, or lossy compression).
*/
@Entity()
export class Copy implements IIdentifiableEntity {
export class Copy implements IdentifiableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -1,7 +1,7 @@
/**
* Entities extending this one have a user-maintained description.
*/
declare interface IDescribableEntity {
interface DescribableEntityInterface {
/**
* a text describing this entity
*/

View File

@ -1,7 +1,7 @@
/**
* Entities implementing this interface build a hierarchy.
*/
declare interface IHierachicalEntity<T> {
interface HierarchicalEntityInterface<T> {
/**
* parent entities
*/

View File

@ -2,7 +2,7 @@
* Every database entity should implement this one.
* It does nothing more but guarantee there is an id column.
*/
declare interface IIdentifiableEntity {
interface IdentifiableEntityInterface {
/**
* the entity id
*/

View File

@ -8,7 +8,7 @@ import { WorkCharacter } from './work-character';
*/
@Entity()
@PercentCheck('weight')
export class InteractionTag implements IIdentifiableEntity, IWeightedEntity {
export class InteractionTag implements IdentifiableEntityInterface, WeightedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -1,7 +1,7 @@
/**
* Entities extending this interface can have multiple names.
*/
declare interface IMultiNamedEntity {
interface MultiNamedEntityInterface {
/**
* the name which is displayed in the user interface
*/
@ -10,5 +10,5 @@ declare interface IMultiNamedEntity {
/**
* other names for the entity
*/
names: Promise<INameEntity[]>;
names: Promise<NameEntityInterface[]>;
}

View File

@ -1,7 +1,7 @@
/**
* This entity describes a single name of an entity with multiple names.
*/
declare interface INameEntity {
interface NameEntityInterface {
/**
* the name
*/
@ -10,5 +10,5 @@ declare interface INameEntity {
/**
* the entity to which the names belong
*/
entity: Promise<IMultiNamedEntity>;
entity: Promise<MultiNamedEntityInterface>;
}

View File

@ -1,7 +1,7 @@
/**
* Entities implementing this interface can be ordered.
*/
declare interface IOrderableEntity {
interface OrderableEntityInterface {
/**
* a lower number means a higher ordering
*/

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Site } from './site';
@Entity()
export class SiteName implements IIdentifiableEntity, INameEntity {
export class SiteName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -6,7 +6,7 @@ import { Source } from './source';
* This non-user-maintained entity describes an online provider of works which can be scraped.
*/
@Entity()
export class Site implements IIdentifiableEntity, IMultiNamedEntity {
export class Site implements IdentifiableEntityInterface, MultiNamedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -6,7 +6,7 @@ import { Site } from './site';
* This entity describes an external source of a copy, in most cases that is a website.
*/
@Entity()
export class Source implements IIdentifiableEntity {
export class Source implements IdentifiableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Tag } from './tag';
@Entity()
export class TagName implements IIdentifiableEntity, INameEntity {
export class TagName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -11,7 +11,12 @@ import { WorkTag } from './work-tag';
* They can also be in a hierarchy
*/
@Entity()
export class Tag implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity, IHierachicalEntity<Tag> {
export class Tag
implements
IdentifiableEntityInterface,
MultiNamedEntityInterface,
DescribableEntityInterface,
HierarchicalEntityInterface<Tag> {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { TransformationType } from './transformation-type';
@Entity()
export class TransformationTypeName implements IIdentifiableEntity, INameEntity {
export class TransformationTypeName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -7,7 +7,8 @@ import { TransformationTypeName } from './transformation-type-name';
* Possible type: translation, decensor, collection, ...
*/
@Entity()
export class TransformationType implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity {
export class TransformationType
implements IdentifiableEntityInterface, MultiNamedEntityInterface, DescribableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -6,7 +6,7 @@ import { Work } from './work';
* This entity describes how one work is transformed to another.
*/
@Entity()
export class Transformation implements IIdentifiableEntity, IOrderableEntity {
export class Transformation implements IdentifiableEntityInterface, OrderableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -1,7 +1,7 @@
/**
* An entity implementing this interface has a weight property.
*/
declare interface IWeightedEntity {
interface WeightedEntityInterface {
/**
* the weight, mathematically a number (0,1], practically between (0,Number.MAX_SAFE_INTEGER]
* the weight can also be not not defined, null in the database

View File

@ -7,7 +7,7 @@ import { Work } from './work';
* This entity connects authors with their work and their role therein.
*/
@Entity()
export class WorkAuthor implements IIdentifiableEntity, IOrderableEntity {
export class WorkAuthor implements IdentifiableEntityInterface, OrderableEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { WorkCharacter } from './work-character';
@Entity()
export class WorkCharacterName implements IIdentifiableEntity, INameEntity {
export class WorkCharacterName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -10,7 +10,7 @@ import { WorldCharacter } from './world-character';
* The character can be original or based on one or more existing characters.
*/
@Entity()
export class WorkCharacter implements IIdentifiableEntity, IMultiNamedEntity {
export class WorkCharacter implements IdentifiableEntityInterface, MultiNamedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Work } from './work';
@Entity()
export class WorkName implements IIdentifiableEntity, INameEntity {
export class WorkName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -8,7 +8,7 @@ import { Work } from './work';
*/
@Entity()
@PercentCheck('weight')
export class WorkTag implements IIdentifiableEntity, IWeightedEntity {
export class WorkTag implements IdentifiableEntityInterface, WeightedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -17,7 +17,7 @@ import { World } from './world';
*/
@Entity()
@PercentCheck('rating')
export class Work implements IIdentifiableEntity, IMultiNamedEntity {
export class Work implements IdentifiableEntityInterface, MultiNamedEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { WorldCharacter } from './world-character';
@Entity()
export class WorldCharacterName implements IIdentifiableEntity, INameEntity {
export class WorldCharacterName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -7,7 +7,8 @@ import { WorldCharacterName } from './world-character-name';
* This entity describes a canon character in a fictional world.
*/
@Entity()
export class WorldCharacter implements IIdentifiableEntity, IMultiNamedEntity, IHierachicalEntity<WorldCharacter> {
export class WorldCharacter
implements IdentifiableEntityInterface, MultiNamedEntityInterface, HierarchicalEntityInterface<WorldCharacter> {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { World } from './world';
@Entity()
export class WorldName implements IIdentifiableEntity, INameEntity {
export class WorldName implements IdentifiableEntityInterface, NameEntityInterface {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -7,7 +7,8 @@ import { WorldName } from './world-name';
* This entity describes a fictional world.
*/
@Entity()
export class World implements IIdentifiableEntity, IMultiNamedEntity, IHierachicalEntity<World> {
export class World
implements IdentifiableEntityInterface, MultiNamedEntityInterface, HierarchicalEntityInterface<World> {
@PrimaryGeneratedColumn()
public id!: number;

View File

@ -1,4 +1,4 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class initialMigration1597705000730 implements MigrationInterface {
name = 'initialMigration1597705000730';

View File

@ -1,4 +1,4 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class initialMigration1587511036078 implements MigrationInterface {
name = 'initialMigration1587511036078';

View File

@ -1,6 +1,6 @@
import BrowserWindow = Electron.BrowserWindow;
import { BrowserWindow } from 'electron';
export interface IAppWindow {
interface AppWindowInterface {
window: BrowserWindow | null;
open(): Promise<void>;
close(force?: boolean): void;

View File

@ -2,8 +2,8 @@ import { app, BrowserWindow, Event, LoadFileOptions, LoadURLOptions, NewWindowWe
import os from 'os';
import path from 'path';
import { isDev } from '../../core/env';
import { ISessionHelper } from '../session/i-session-helper';
import { IAppWindow } from './i-app-window';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import type { AppWindowInterface } from './app-window-interface';
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
let defaultOptions: BrowserWindowConstructorOptions = {
@ -30,12 +30,12 @@ switch (os.platform()) {
break;
}
export abstract class AppWindow implements IAppWindow {
export abstract class AppWindow implements AppWindowInterface {
protected static default = {};
protected _window: BrowserWindow | null = null;
protected readonly sessionHelper: ISessionHelper;
protected readonly sessionHelper: SessionHelperInterface;
protected options: BrowserWindowConstructorOptions;
@ -43,7 +43,11 @@ export abstract class AppWindow implements IAppWindow {
protected abstract loadOptions: LoadFileOptions | LoadURLOptions;
protected constructor(sessionHelper: ISessionHelper, uri: string, options: BrowserWindowConstructorOptions = {}) {
protected constructor(
sessionHelper: SessionHelperInterface,
uri: string,
options: BrowserWindowConstructorOptions = {}
) {
this.sessionHelper = sessionHelper;
this.options = { ...defaultOptions, ...options };
this.uri = uri;
@ -85,7 +89,7 @@ export abstract class AppWindow implements IAppWindow {
return !this._window;
}
protected getCsp(): IContentSecurityPolicy {
protected getCsp(): Session.ContentSecurityPolicy {
return {};
}

View File

@ -1,12 +1,12 @@
import { BrowserWindow, BrowserWindowConstructorOptions, LoadFileOptions } from 'electron';
import { ISessionHelper } from '../session/i-session-helper';
import type { BrowserWindow, BrowserWindowConstructorOptions, LoadFileOptions } from 'electron';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import { AppWindow } from './app-window';
export abstract class FileAppWindow extends AppWindow {
protected loadOptions: LoadFileOptions;
protected constructor(
sessionHelper: ISessionHelper,
sessionHelper: SessionHelperInterface,
uri: string,
options: BrowserWindowConstructorOptions = {},
loadOptions: LoadFileOptions = {}

View File

@ -1,8 +0,0 @@
import { IUrlAppWindow } from './i-url-app-window';
export interface ISiteAppWindow extends IUrlAppWindow {
/**
* @see IMutex.acquire
*/
acquireLock(): Promise<Mutex.ReleaseFunction>;
}

View File

@ -1,8 +0,0 @@
import { LoadURLOptions } from 'electron';
import { IAppWindow } from './i-app-window';
export interface IUrlAppWindow extends IAppWindow {
downloadUrlSafe(url: string, savePath: string, options?: LoadURLOptions): Promise<void>;
loadUrlSafe(url: string, options?: LoadURLOptions): Promise<void>;
}

View File

@ -1,11 +1,11 @@
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { ISessionHelper } from '../session/i-session-helper';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import { FileAppWindow } from './file-app-window';
@injectable()
export class MainAppWindow extends FileAppWindow {
public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
public constructor(@inject('session-helper') sessionHelper: SessionHelperInterface) {
super(sessionHelper, 'frontend/index.html', {
webPreferences: {
nodeIntegration: true,

View File

@ -0,0 +1,8 @@
import type { UrlAppWindowInterface } from './url-app-window-interface';
interface SiteAppWindowInterface extends UrlAppWindowInterface {
/**
* @see IMutex.acquire
*/
acquireLock(): Promise<Mutex.ReleaseFunction>;
}

View File

@ -1,18 +1,18 @@
import { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import { SimpleMutex } from '../mutex/simple-mutex';
import { ISessionHelper } from '../session/i-session-helper';
import { ISiteAppWindow } from './i-site-app-window';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import type { SiteAppWindowInterface } from './site-app-window-interface';
import { UrlAppWindow } from './url-app-window';
/**
* This class represents an app window of a site which need to be crawled via the built-in chromium of Electron.
* It offers a lock so that multiple calls do executed simultaneously on the same chromium window.
*/
export abstract class SiteAppWindow extends UrlAppWindow implements ISiteAppWindow {
private windowLock: IMutex;
export abstract class SiteAppWindow extends UrlAppWindow implements SiteAppWindowInterface {
private windowLock: MutexInterface;
protected constructor(
sessionHelper: ISessionHelper,
sessionHelper: SessionHelperInterface,
uri: string,
options: BrowserWindowConstructorOptions = {},
loadOptions: LoadURLOptions = {}

View File

@ -0,0 +1,8 @@
import type { LoadURLOptions } from 'electron';
import type { AppWindowInterface } from './app-window-interface';
interface UrlAppWindowInterface extends AppWindowInterface {
downloadUrlSafe(url: string, savePath: string, options?: LoadURLOptions): Promise<void>;
loadUrlSafe(url: string, options?: LoadURLOptions): Promise<void>;
}

View File

@ -1,12 +1,12 @@
import { BrowserWindow, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import type { BrowserWindow, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import { promisify } from 'util';
import { ISessionHelper } from '../session/i-session-helper';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import { AppWindow } from './app-window';
import { IUrlAppWindow } from './i-url-app-window';
import type { UrlAppWindowInterface } from './url-app-window-interface';
import { WindowClosedError } from './window-closed-error';
import Timeout = NodeJS.Timeout;
export abstract class UrlAppWindow extends AppWindow implements IUrlAppWindow {
export abstract class UrlAppWindow extends AppWindow implements UrlAppWindowInterface {
protected loadOptions: LoadURLOptions;
/**
@ -32,7 +32,7 @@ export abstract class UrlAppWindow extends AppWindow implements IUrlAppWindow {
private loadWait: Promise<void> = Promise.resolve();
protected constructor(
sessionHelper: ISessionHelper,
sessionHelper: SessionHelperInterface,
uri: string,
options: BrowserWindowConstructorOptions = {},
loadOptions: LoadURLOptions = {}

View File

@ -1,5 +1,5 @@
import { dialog, OpenDialogOptions } from 'electron';
interface IDialog {
interface DialogInterface {
selectFolder(options: OpenDialogOptions): ReturnType<typeof dialog.showOpenDialog>;
}

View File

@ -1,14 +1,13 @@
import { dialog, OpenDialogOptions } from 'electron';
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { II18nTranslator } from '../i18n/i-i18n-translator';
import { IDialog } from './i-dialog';
import type { DialogInterface } from './dialog-interface';
@injectable()
export class Dialog implements IDialog {
private readonly translator: II18nTranslator;
export class Dialog implements DialogInterface {
private readonly translator: I18nTranslatorInterface;
public constructor(@inject('i18n-translator') translator: II18nTranslator) {
public constructor(@inject('i18n-translator') translator: I18nTranslatorInterface) {
this.translator = translator;
}

View File

@ -1,3 +0,0 @@
export interface II18nTranslator {
t(text: string): string;
}

View File

@ -0,0 +1,3 @@
interface I18nTranslatorInterface {
t(text: string): string;
}

View File

@ -1,8 +1,7 @@
import { injectable } from 'inversify';
import { II18nTranslator } from './i-i18n-translator';
@injectable()
export class I18nTranslator implements II18nTranslator {
export class I18nTranslator implements I18nTranslatorInterface {
public t(text: string): string {
return text;
}

View File

@ -1,7 +1,7 @@
import { registerHandler } from '../ipc-server';
export function answer(channel: IpcChannel): IpcControllerMethodDecorator {
return function (target: IIpcController, propertyKey): void {
return function (target: IpcController, propertyKey): void {
registerHandler(channel, target, propertyKey);
};
}

View File

@ -1,12 +1,12 @@
import { ipcMain } from 'electron';
import IpcMainEvent = Electron.IpcMainEvent;
export function registerHandler(channel: IpcChannel, controller: IIpcController, handler: string): void {
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
export function registerHandler(channel: IpcChannel, controller: IpcController, handler: string): void {
ipcMain.on(channel, (event: IpcMainEvent, payload: IpcPayload) => {
((controller.get() as unknown) as { [x: string]: IpcHandler })
[handler](payload.data)
.then((result: unknown) => {
const response: IIpcResponse = {
const response: IpcResponse = {
id: payload.id,
success: true,
data: result,
@ -14,7 +14,7 @@ export function registerHandler(channel: IpcChannel, controller: IIpcController,
event.reply(channel, response);
})
.catch((reason: Error) => {
const response: IIpcResponse = {
const response: IpcResponse = {
id: payload.id,
success: false,
error: reason.message,

View File

@ -1,8 +1,8 @@
/**
* A Logger provides methods to save a developer message somewhere it can be retrieved
* @see ILogger.getLogFile
* @see LoggerInterface.getLogFile
*/
export interface ILogger {
interface LoggerInterface {
/**
* default logging method, the logging level needs to be specified
* @see LogLevel
@ -47,7 +47,7 @@ export interface ILogger {
/**
* logs the error, preferably with stack trace
* @see ILogger.getExceptionsLogFile
* @see LoggerInterface.getExceptionsLogFile
*/
exception(error: Error): Promise<void>;

View File

@ -1,11 +1,10 @@
import { injectable } from 'inversify';
import { ILogger } from './i-logger';
/**
* Mock of a logger, does not log anywhere
*/
@injectable()
export class LoggerMock implements ILogger {
export class LoggerMock implements LoggerInterface {
public getLogFile(): string {
return '';
}

View File

@ -6,7 +6,6 @@ import fc from 'fast-check';
import fs from 'fs-extra';
import { container } from '../../core/container';
import { setDev } from '../../core/env.spec';
import { ILogger } from './i-logger';
import { LogLevel } from './log-level';
chai.use(chaiFs);
@ -25,7 +24,7 @@ describe('Logger Service', () => {
});
}
function getDefaultLogReadLineInterface(logger: ILogger) {
function getDefaultLogReadLineInterface(logger: LoggerInterface) {
return createInterface(fs.createReadStream(logger.getLogFile()));
}
@ -42,20 +41,20 @@ describe('Logger Service', () => {
const logLevelNumberArbitrary = fc.constantFrom(...logLevelsNumber);
it('creates log files', () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
expect(logger.getLogFile()).path('log file is not created');
expect(logger.getExceptionsLogFile()).path('exception log file is not created');
});
it('logs exceptions', async () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
await logger.exception(new Error('this is an error'));
});
it("default log file doesn't get bigger than 50KB @slow", async () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
let prevLogFileSize = (await fs.stat(logger.getLogFile())).size;
let minNumberOfLines = maxLogSize;
@ -76,7 +75,7 @@ describe('Logger Service', () => {
}).timeout(15000);
it("exception log file doesn't get bigger than 50KB @slow", async () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
let prevLogFileSize = (await fs.stat(logger.getExceptionsLogFile())).size;
let minNumberOfLines = maxLogSize;
@ -97,7 +96,7 @@ describe('Logger Service', () => {
}).timeout(15000);
it('logs different levels directly', () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
return fc.assert(
fc.asyncProperty(logLevelArbitrary as LogLevelArbitrary, fc.string(), async (logLevel, message) => {
@ -113,7 +112,7 @@ describe('Logger Service', () => {
});
it('logs different levels indirectly via the generic log function', () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
return fc.assert(
fc.asyncProperty(logLevelNumberArbitrary, fc.string(), async (logLevelNumber, message) => {
@ -132,7 +131,7 @@ describe('Logger Service', () => {
});
it('logs debug only in dev mode', async () => {
const logger: ILogger = container.get('logger');
const logger: LoggerInterface = container.get('logger');
setDev();
await logger.debug('this is a development message');

View File

@ -4,7 +4,6 @@ import * as fs from 'fs-extra';
import { injectable } from 'inversify';
import { appPath } from '../../core/app-path';
import { isDev } from '../../core/env';
import { ILogger } from './i-logger';
import { LogLevel } from './log-level';
const loggingDir = path.resolve(appPath, 'logs');
@ -16,7 +15,7 @@ const maxLogSize = 50000;
* A logger not using winston to log to files in the appPath.
*/
@injectable()
export class Logger implements ILogger {
export class Logger implements LoggerInterface {
public constructor() {
fs.createFileSync(logFile);
fs.createFileSync(exceptionLogFile);

View File

@ -3,7 +3,7 @@
*
* Acquiring this lock returns a release function function (via promise) which needs to be called to release the lock again.
*/
interface IMutex {
interface MutexInterface {
/**
* acquires the lock and returns a Promise with the release function to be called when the lock shall be released.
* This release function needs to be called or the lock will never release and execution of subsequent consumers will not take place.

View File

@ -1,7 +1,7 @@
/**
* This class implements a simple mutex using a semaphore.
*/
export class SimpleMutex implements IMutex {
export class SimpleMutex implements MutexInterface {
/**
* This queue is an array of promise resolve functions.
* Calling them signals the corresponding consumer that the lock is now free.

View File

@ -1,5 +0,0 @@
import { ISiteAppWindow } from '../app-window/i-site-app-window';
export interface INhentaiAppWindow extends ISiteAppWindow {
getFavorites(): Promise<NodeJS.ReadableStream>;
}

View File

@ -1,3 +1,3 @@
export interface INhentaiApi {
interface NhentaiApiInterface {
getFavorites(): Promise<NodeJS.ReadableStream>;
}

View File

@ -1,13 +1,12 @@
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { INhentaiApi } from './i-nhentai-api';
import { INhentaiAppWindow } from './i-nhentai-app-window';
import type { NhentaiAppWindowInterface } from './nhentai-app-window-interface';
@injectable()
export class NhentaiApi implements INhentaiApi {
private readonly appWindow: INhentaiAppWindow;
export class NhentaiApi implements NhentaiApiInterface {
private readonly appWindow: NhentaiAppWindowInterface;
public constructor(@inject('nhentai-app-window') appWindow: INhentaiAppWindow) {
public constructor(@inject('nhentai-app-window') appWindow: NhentaiAppWindowInterface) {
this.appWindow = appWindow;
}

View File

@ -0,0 +1,5 @@
import type { SiteAppWindowInterface } from '../app-window/site-app-window-interface';
interface NhentaiAppWindowInterface extends SiteAppWindowInterface {
getFavorites(): Promise<NodeJS.ReadableStream>;
}

View File

@ -1,4 +1,4 @@
import { WebContents } from 'electron';
import type { WebContents } from 'electron';
import os from 'os';
import path from 'path';
import { Readable } from 'stream';
@ -8,9 +8,8 @@ import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { SiteAppWindow } from '../app-window/site-app-window';
import { WindowClosedError } from '../app-window/window-closed-error';
import { ISessionHelper } from '../session/i-session-helper';
import { INhentaiAppWindow } from './i-nhentai-app-window';
import { IFavorite } from './nhentai';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import type { NhentaiAppWindowInterface } from './nhentai-app-window-interface';
import {
url as nhentaiUrl,
hostname as nhentaiHostname,
@ -25,8 +24,8 @@ import {
const waitInterval = 2000;
@injectable()
export class NhentaiAppWindow extends SiteAppWindow implements INhentaiAppWindow {
public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
export class NhentaiAppWindow extends SiteAppWindow implements NhentaiAppWindowInterface {
public constructor(@inject('session-helper') sessionHelper: SessionHelperInterface) {
super(sessionHelper, nhentaiUrl);
}
@ -52,7 +51,7 @@ export class NhentaiAppWindow extends SiteAppWindow implements INhentaiAppWindow
}
const readable = Readable.from(
(async function* (thisArg): AsyncGenerator<IFavorite, undefined> {
(async function* (thisArg): AsyncGenerator<Nhentai.Favorite, undefined> {
for (let i = 0; i < bookUrls.length; i++) {
const bookUrl = bookUrls[i];
yield await thisArg.getBookTorrent(bookUrl);
@ -76,7 +75,7 @@ export class NhentaiAppWindow extends SiteAppWindow implements INhentaiAppWindow
}
}
protected getCsp(): IContentSecurityPolicy {
protected getCsp(): Session.ContentSecurityPolicy {
return {
'default-src': ['nhentai.net'],
'script-src': ['nhentai.net', "'unsafe-eval'"],
@ -163,7 +162,7 @@ export class NhentaiAppWindow extends SiteAppWindow implements INhentaiAppWindow
return;
}
private async getBookTorrent(bookUrl: string): Promise<IFavorite> {
private async getBookTorrent(bookUrl: string): Promise<Nhentai.Favorite> {
if (!this._window) {
throw new WindowClosedError();
}

View File

@ -1,20 +1,17 @@
import path from 'path';
import { createWriteStream } from 'fs-extra';
import { container } from '../../core/container';
import { IDialog } from '../dialog/i-dialog';
import { II18nTranslator } from '../i18n/i-i18n-translator';
import type { DialogInterface } from '../dialog/dialog-interface';
import { answer } from '../ipc/annotations/answer';
import { INhentaiApi } from './i-nhentai-api';
import { IFavorite } from './nhentai';
export class NhentaiIpcController implements IIpcController {
private readonly nhentaiApi: INhentaiApi;
export class NhentaiIpcController implements IpcController {
private readonly nhentaiApi: NhentaiApiInterface;
private readonly translator: II18nTranslator;
private readonly translator: I18nTranslatorInterface;
private readonly dialog: IDialog;
private readonly dialog: DialogInterface;
private constructor(nhentaiApi: INhentaiApi, translator: II18nTranslator, dialog: IDialog) {
private constructor(nhentaiApi: NhentaiApiInterface, translator: I18nTranslatorInterface, dialog: DialogInterface) {
this.nhentaiApi = nhentaiApi;
this.translator = translator;
this.dialog = dialog;
@ -33,7 +30,7 @@ export class NhentaiIpcController implements IIpcController {
const favoritesStream = await this.nhentaiApi.getFavorites();
return new Promise((resolve) => {
favoritesStream.on('data', (favorite: IFavorite) => {
favoritesStream.on('data', (favorite: Nhentai.Favorite) => {
const writable = createWriteStream(path.resolve(result.filePaths[0], favorite.name));
favorite.torrentFile.pipe(writable);
});
@ -43,9 +40,9 @@ export class NhentaiIpcController implements IIpcController {
}
public get(): NhentaiIpcController {
const nhentaiApi: INhentaiApi = container.get('nhentai-api');
const translator: II18nTranslator = container.get('i18n-translator');
const dialog: IDialog = container.get('dialog');
const nhentaiApi: NhentaiApiInterface = container.get('nhentai-api');
const translator: I18nTranslatorInterface = container.get('i18n-translator');
const dialog: DialogInterface = container.get('dialog');
return new NhentaiIpcController(nhentaiApi, translator, dialog);
}
}

View File

@ -1,4 +1,6 @@
export interface IFavorite {
name: string;
torrentFile: NodeJS.ReadableStream;
declare namespace Nhentai {
type Favorite = {
name: string;
torrentFile: NodeJS.ReadableStream;
};
}

View File

@ -1,5 +0,0 @@
import { BrowserWindow } from 'electron';
export interface ISessionHelper {
setCsp(window: BrowserWindow, csp: IContentSecurityPolicy): void;
}

View File

@ -0,0 +1,5 @@
import { BrowserWindow } from 'electron';
interface SessionHelperInterface {
setCsp(window: BrowserWindow, csp: Session.ContentSecurityPolicy): void;
}

View File

@ -1,23 +1,26 @@
import { injectable } from 'inversify';
import { isDev } from '../../core/env';
import { ISessionHelper } from './i-session-helper';
import type { SessionHelperInterface } from './session-helper-interface';
const defaultCsp: IContentSecurityPolicy = {
const defaultCsp: Session.ContentSecurityPolicy = {
'default-src': ["'self'"],
'style-src': ["'unsafe-inline'"],
'object-src': ["'none'"],
};
@injectable()
export class SessionHelper implements ISessionHelper {
private static stringifyCspHeader(csp: IContentSecurityPolicy): string {
export class SessionHelper implements SessionHelperInterface {
private static stringifyCspHeader(csp: Session.ContentSecurityPolicy): string {
return Object.entries(csp)
.map((directive: [string, CspValue[]]) => `${directive[0]} ${directive[1]?.join(' ')}`)
.map(
(directive: [string, Session.CspValue[] | undefined]) =>
`${directive[0]} ${directive[1] ? directive[1]?.join(' ') : ''}`
)
.join('; ');
}
public setCsp(window: Electron.BrowserWindow, csp: IContentSecurityPolicy): void {
const mergedCsp: IContentSecurityPolicy = { ...defaultCsp, ...csp };
public setCsp(window: Electron.BrowserWindow, csp: Session.ContentSecurityPolicy): void {
const mergedCsp: Session.ContentSecurityPolicy = { ...defaultCsp, ...csp };
if (isDev()) {
mergedCsp['default-src'] = ['devtools:'].concat(mergedCsp['default-src'] ?? []);

View File

@ -1,23 +1,25 @@
type CspValue = '*' | "'none'" | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" | string;
declare namespace Session {
type CspValue = '*' | "'none'" | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" | string;
/**
* This interface represents a content security policy.
*
* @see https://www.w3.org/TR/CSP/
* @see https://content-security-policy.com/
*/
interface IContentSecurityPolicy {
'child-src'?: CspValue[];
'connect-src'?: CspValue[];
'default-src'?: CspValue[];
'font-src'?: CspValue[];
'frame-src'?: CspValue[];
'img-src'?: CspValue[];
'media-src'?: CspValue[];
'object-src'?: ["'none'"];
'script-src'?: CspValue[];
'script-src-elem'?: CspValue[];
'script-src-attr'?: CspValue[];
'style-src'?: CspValue[];
'worker-src'?: CspValue[];
/**
* This interface represents a content security policy.
*
* @see https://www.w3.org/TR/CSP/
* @see https://content-security-policy.com/
*/
type ContentSecurityPolicy = {
'child-src'?: CspValue[];
'connect-src'?: CspValue[];
'default-src'?: CspValue[];
'font-src'?: CspValue[];
'frame-src'?: CspValue[];
'img-src'?: CspValue[];
'media-src'?: CspValue[];
'object-src'?: ["'none'"];
'script-src'?: CspValue[];
'script-src-elem'?: CspValue[];
'script-src-attr'?: CspValue[];
'style-src'?: CspValue[];
'worker-src'?: CspValue[];
};
}

View File

@ -1,4 +1,4 @@
export interface IStore {
interface StoreInterface {
load: (key: StoreKey) => Promise<unknown>;
save: (key: StoreKey, data: unknown) => Promise<void>;
}

View File

@ -1,11 +1,10 @@
import { injectable } from 'inversify';
import { IStore } from './i-store';
/**
* This mock store saves the data in memory.
*/
@injectable()
export class StoreMock implements IStore {
export class StoreMock implements StoreInterface {
private store: { [x in StoreKey]?: unknown } = {};
public load(key: StoreKey): Promise<unknown> {

View File

@ -1,13 +1,12 @@
import { expect } from 'chai';
import 'mocha';
import { container } from '../../core/container';
import { IStore } from './i-store';
describe('Store Service', function () {
this.timeout(10000);
it('loads saved data', () => {
const store: IStore = container.get('store');
const store: StoreInterface = container.get('store');
const testData = {
something: 'gaga',
somethingElse: 0,

View File

@ -1,12 +1,11 @@
import { injectable } from 'inversify';
import { Database, getConnection } from '../../core/database';
import { StoreValue } from '../../entities/store/store-value';
import { IStore } from './i-store';
const CACHE_ID = 'store';
@injectable()
export class Store implements IStore {
export class Store implements StoreInterface {
public async load(key: StoreKey): Promise<unknown> {
const c = await getConnection(Database.STORE);
const repository = c.getRepository(StoreValue);

View File

@ -2,16 +2,16 @@ import { ipcRenderer } from 'electron';
import { uuid } from '../../services/uuid';
import IpcRendererEvent = Electron.IpcRendererEvent;
const ipcClient: IIpcClient = {
const ipcClient: IpcClient = {
ask: (channel: IpcChannel, data?: unknown): Promise<unknown> => {
const id = uuid();
const payload: IIpcPayload = {
const payload: IpcPayload = {
id,
data,
};
return new Promise((resolve: (value?: unknown) => void, reject: (reason?: Error) => void): void => {
const listener = (event: IpcRendererEvent, response: IIpcResponse): void => {
const listener = (event: IpcRendererEvent, response: IpcResponse): void => {
if (response.id === id) {
if (response.success) {
resolve(response.data);

View File

@ -12,7 +12,8 @@
"sourceMap": true,
"preserveConstEnums": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
"emitDecoratorMetadata": true,
"importsNotUsedAsValues": "error"
},
"include": ["declarations/**/*.ts", "types/**/*.ts", "src/main.ts", "src/main/**/*.ts", "src/services/**/*.ts"]
}

View File

@ -6,7 +6,8 @@
"noImplicitAny": true,
"removeComments": true,
"sourceMap": true,
"preserveConstEnums": false
"preserveConstEnums": false,
"importsNotUsedAsValues": "error"
},
"include": [
"declarations/**/*.ts",

View File

@ -2,7 +2,7 @@
* @see MethodDecorator
*/
type IpcControllerMethodDecorator = <T>(
target: IIpcController,
target: IpcController,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>
) => void;

19
types/ipc.d.ts vendored
View File

@ -2,26 +2,25 @@ declare const enum IpcChannel {
NHENTAI_SAVE_FAVORITES = 'NHENTAI_SAVE_FAVORITES',
}
interface IIpcPayload {
type IpcPayload = {
id: string;
data: unknown;
}
};
interface IIpcResponse {
type IpcResponse = {
id: string;
success: boolean;
data?: unknown;
// just the error message
error?: string;
}
};
interface IIpcClient {
type IpcClient = {
ask: (channel: IpcChannel, data?: unknown) => Promise<unknown>;
}
};
type IpcHandler = (data?: unknown) => Promise<unknown>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/typescript-eslint/typescript-eslint/issues/2714
interface IIpcController {
get(): IIpcController;
}
type IpcController = {
get(): IpcController;
};