aboutsummaryrefslogtreecommitdiff
path: root/src/content/usecases
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-19 15:59:05 +0900
committerGitHub <noreply@github.com>2019-05-19 15:59:05 +0900
commit3f4bc62ed515f1c5da90ee1c3e42f3d435ea6e39 (patch)
tree8af9f8e5b12d007ce9628b40f3046b73f18e29f8 /src/content/usecases
parent6ec560bca33e774ff7e363270c423c919fdcf4ce (diff)
parentc4dcdff9844e2404e3bc035f4cea9fce2f7770ab (diff)
Merge pull request #587 from ueokande/refactor-content
Refactor content scripts
Diffstat (limited to 'src/content/usecases')
-rw-r--r--src/content/usecases/AddonEnabledUseCase.ts40
-rw-r--r--src/content/usecases/ClipboardUseCase.ts44
-rw-r--r--src/content/usecases/ConsoleFrameUseCase.ts17
-rw-r--r--src/content/usecases/FindSlaveUseCase.ts20
-rw-r--r--src/content/usecases/FindUseCase.ts81
-rw-r--r--src/content/usecases/FocusUseCase.ts16
-rw-r--r--src/content/usecases/FollowMasterUseCase.ts150
-rw-r--r--src/content/usecases/FollowSlaveUseCase.ts91
-rw-r--r--src/content/usecases/HintKeyProducer.ts38
-rw-r--r--src/content/usecases/KeymapUseCase.ts87
-rw-r--r--src/content/usecases/MarkKeyUseCase.ts36
-rw-r--r--src/content/usecases/MarkUseCase.ts66
-rw-r--r--src/content/usecases/NavigateUseCase.ts36
-rw-r--r--src/content/usecases/ScrollUseCase.ts58
-rw-r--r--src/content/usecases/SettingUseCase.ts24
15 files changed, 804 insertions, 0 deletions
diff --git a/src/content/usecases/AddonEnabledUseCase.ts b/src/content/usecases/AddonEnabledUseCase.ts
new file mode 100644
index 0000000..e9ce0a6
--- /dev/null
+++ b/src/content/usecases/AddonEnabledUseCase.ts
@@ -0,0 +1,40 @@
+import AddonIndicatorClient, { AddonIndicatorClientImpl }
+ from '../client/AddonIndicatorClient';
+import AddonEnabledRepository, { AddonEnabledRepositoryImpl }
+ from '../repositories/AddonEnabledRepository';
+
+export default class AddonEnabledUseCase {
+ private indicator: AddonIndicatorClient;
+
+ private repository: AddonEnabledRepository;
+
+ constructor({
+ indicator = new AddonIndicatorClientImpl(),
+ repository = new AddonEnabledRepositoryImpl(),
+ } = {}) {
+ this.indicator = indicator;
+ this.repository = repository;
+ }
+
+ async enable(): Promise<void> {
+ await this.setEnabled(true);
+ }
+
+ async disable(): Promise<void> {
+ await this.setEnabled(false);
+ }
+
+ async toggle(): Promise<void> {
+ let current = this.repository.get();
+ await this.setEnabled(!current);
+ }
+
+ getEnabled(): boolean {
+ return this.repository.get();
+ }
+
+ private async setEnabled(on: boolean): Promise<void> {
+ this.repository.set(on);
+ await this.indicator.setEnabled(on);
+ }
+}
diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts
new file mode 100644
index 0000000..b2ece2f
--- /dev/null
+++ b/src/content/usecases/ClipboardUseCase.ts
@@ -0,0 +1,44 @@
+import * as urls from '../../shared/urls';
+import ClipboardRepository, { ClipboardRepositoryImpl }
+ from '../repositories/ClipboardRepository';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+import TabsClient, { TabsClientImpl }
+ from '../client/TabsClient';
+import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient';
+
+export default class ClipboardUseCase {
+ private repository: ClipboardRepository;
+
+ private settingRepository: SettingRepository;
+
+ private client: TabsClient;
+
+ private consoleClient: ConsoleClient;
+
+ constructor({
+ repository = new ClipboardRepositoryImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ client = new TabsClientImpl(),
+ consoleClient = new ConsoleClientImpl(),
+ } = {}) {
+ this.repository = repository;
+ this.settingRepository = settingRepository;
+ this.client = client;
+ this.consoleClient = consoleClient;
+ }
+
+ async yankCurrentURL(): Promise<string> {
+ let url = window.location.href;
+ this.repository.write(url);
+ await this.consoleClient.info('Yanked ' + url);
+ return Promise.resolve(url);
+ }
+
+ async openOrSearch(newTab: boolean): Promise<void> {
+ let search = this.settingRepository.get().search;
+ let text = this.repository.read();
+ let url = urls.searchUrl(text, search);
+ await this.client.openUrl(url, newTab);
+ }
+}
diff --git a/src/content/usecases/ConsoleFrameUseCase.ts b/src/content/usecases/ConsoleFrameUseCase.ts
new file mode 100644
index 0000000..b4c756c
--- /dev/null
+++ b/src/content/usecases/ConsoleFrameUseCase.ts
@@ -0,0 +1,17 @@
+import ConsoleFramePresenter, { ConsoleFramePresenterImpl }
+ from '../presenters/ConsoleFramePresenter';
+
+export default class ConsoleFrameUseCase {
+ private consoleFramePresenter: ConsoleFramePresenter;
+
+ constructor({
+ consoleFramePresenter = new ConsoleFramePresenterImpl(),
+ } = {}) {
+ this.consoleFramePresenter = consoleFramePresenter;
+ }
+
+ unfocus() {
+ window.focus();
+ this.consoleFramePresenter.blur();
+ }
+}
diff --git a/src/content/usecases/FindSlaveUseCase.ts b/src/content/usecases/FindSlaveUseCase.ts
new file mode 100644
index 0000000..b733cbd
--- /dev/null
+++ b/src/content/usecases/FindSlaveUseCase.ts
@@ -0,0 +1,20 @@
+import FindMasterClient, { FindMasterClientImpl }
+ from '../client/FindMasterClient';
+
+export default class FindSlaveUseCase {
+ private findMasterClient: FindMasterClient;
+
+ constructor({
+ findMasterClient = new FindMasterClientImpl(),
+ } = {}) {
+ this.findMasterClient = findMasterClient;
+ }
+
+ findNext() {
+ this.findMasterClient.findNext();
+ }
+
+ findPrev() {
+ this.findMasterClient.findPrev();
+ }
+}
diff --git a/src/content/usecases/FindUseCase.ts b/src/content/usecases/FindUseCase.ts
new file mode 100644
index 0000000..74cbc97
--- /dev/null
+++ b/src/content/usecases/FindUseCase.ts
@@ -0,0 +1,81 @@
+import FindPresenter, { FindPresenterImpl } from '../presenters/FindPresenter';
+import FindRepository, { FindRepositoryImpl }
+ from '../repositories/FindRepository';
+import FindClient, { FindClientImpl } from '../client/FindClient';
+import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient';
+
+export default class FindUseCase {
+ private presenter: FindPresenter;
+
+ private repository: FindRepository;
+
+ private client: FindClient;
+
+ private consoleClient: ConsoleClient;
+
+ constructor({
+ presenter = new FindPresenterImpl() as FindPresenter,
+ repository = new FindRepositoryImpl(),
+ client = new FindClientImpl(),
+ consoleClient = new ConsoleClientImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ this.repository = repository;
+ this.client = client;
+ this.consoleClient = consoleClient;
+ }
+
+ async startFind(keyword?: string): Promise<void> {
+ this.presenter.clearSelection();
+ if (keyword) {
+ this.saveKeyword(keyword);
+ } else {
+ let lastKeyword = await this.getKeyword();
+ if (!lastKeyword) {
+ return this.showNoLastKeywordError();
+ }
+ this.saveKeyword(lastKeyword);
+ }
+ return this.findNext();
+ }
+
+ findNext(): Promise<void> {
+ return this.findNextPrev(false);
+ }
+
+ findPrev(): Promise<void> {
+ return this.findNextPrev(true);
+ }
+
+ private async findNextPrev(
+ backwards: boolean,
+ ): Promise<void> {
+ let keyword = await this.getKeyword();
+ if (!keyword) {
+ return this.showNoLastKeywordError();
+ }
+ let found = this.presenter.find(keyword, backwards);
+ if (found) {
+ this.consoleClient.info('Pattern found: ' + keyword);
+ } else {
+ this.consoleClient.error('Pattern not found: ' + keyword);
+ }
+ }
+
+ private async getKeyword(): Promise<string | null> {
+ let keyword = this.repository.getLastKeyword();
+ if (!keyword) {
+ keyword = await this.client.getGlobalLastKeyword();
+ }
+ return keyword;
+ }
+
+ private async saveKeyword(keyword: string): Promise<void> {
+ this.repository.setLastKeyword(keyword);
+ await this.client.setGlobalLastKeyword(keyword);
+ }
+
+ private async showNoLastKeywordError(): Promise<void> {
+ await this.consoleClient.error('No previous search keywords');
+ }
+}
diff --git a/src/content/usecases/FocusUseCase.ts b/src/content/usecases/FocusUseCase.ts
new file mode 100644
index 0000000..0ad4021
--- /dev/null
+++ b/src/content/usecases/FocusUseCase.ts
@@ -0,0 +1,16 @@
+import FocusPresenter, { FocusPresenterImpl }
+ from '../presenters/FocusPresenter';
+
+export default class FocusUseCases {
+ private presenter: FocusPresenter;
+
+ constructor({
+ presenter = new FocusPresenterImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ }
+
+ focusFirstInput() {
+ this.presenter.focusFirstElement();
+ }
+}
diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts
new file mode 100644
index 0000000..9cbb790
--- /dev/null
+++ b/src/content/usecases/FollowMasterUseCase.ts
@@ -0,0 +1,150 @@
+import FollowKeyRepository, { FollowKeyRepositoryImpl }
+ from '../repositories/FollowKeyRepository';
+import FollowMasterRepository, { FollowMasterRepositoryImpl }
+ from '../repositories/FollowMasterRepository';
+import FollowSlaveClient, { FollowSlaveClientImpl }
+ from '../client/FollowSlaveClient';
+import HintKeyProducer from './HintKeyProducer';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+
+export default class FollowMasterUseCase {
+ private followKeyRepository: FollowKeyRepository;
+
+ private followMasterRepository: FollowMasterRepository;
+
+ private settingRepository: SettingRepository;
+
+ // TODO Make repository
+ private producer: HintKeyProducer | null;
+
+ constructor({
+ followKeyRepository = new FollowKeyRepositoryImpl(),
+ followMasterRepository = new FollowMasterRepositoryImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ } = {}) {
+ this.followKeyRepository = followKeyRepository;
+ this.followMasterRepository = followMasterRepository;
+ this.settingRepository = settingRepository;
+ this.producer = null;
+ }
+
+ startFollow(newTab: boolean, background: boolean): void {
+ let hintchars = this.settingRepository.get().properties.hintchars;
+ this.producer = new HintKeyProducer(hintchars);
+
+ this.followKeyRepository.clearKeys();
+ this.followMasterRepository.setCurrentFollowMode(newTab, background);
+
+ let viewWidth = window.top.innerWidth;
+ let viewHeight = window.top.innerHeight;
+ new FollowSlaveClientImpl(window.top).requestHintCount(
+ { width: viewWidth, height: viewHeight },
+ { x: 0, y: 0 },
+ );
+
+ let frameElements = window.document.querySelectorAll('iframe');
+ for (let i = 0; i < frameElements.length; ++i) {
+ let ele = frameElements[i] as HTMLFrameElement | HTMLIFrameElement;
+ let { left: frameX, top: frameY } = ele.getBoundingClientRect();
+ new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount(
+ { width: viewWidth, height: viewHeight },
+ { x: frameX, y: frameY },
+ );
+ }
+ }
+
+ // eslint-disable-next-line max-statements
+ createSlaveHints(count: number, sender: Window): void {
+ let produced = [];
+ for (let i = 0; i < count; ++i) {
+ let tag = this.producer!!.produce();
+ produced.push(tag);
+ this.followMasterRepository.addTag(tag);
+ }
+
+ let doc = window.document;
+ let viewWidth = window.innerWidth || doc.documentElement.clientWidth;
+ let viewHeight = window.innerHeight || doc.documentElement.clientHeight;
+ let pos = { x: 0, y: 0 };
+ if (sender !== window) {
+ let frameElements = window.document.querySelectorAll('iframe');
+ let ele = Array.from(frameElements).find(e => e.contentWindow === sender);
+ if (!ele) {
+ // elements of the sender is gone
+ return;
+ }
+ let { left: frameX, top: frameY } = ele.getBoundingClientRect();
+ pos = { x: frameX, y: frameY };
+ }
+ new FollowSlaveClientImpl(sender).createHints(
+ { width: viewWidth, height: viewHeight },
+ pos,
+ produced,
+ );
+ }
+
+ cancelFollow(): void {
+ this.followMasterRepository.clearTags();
+ this.broadcastToSlaves((client) => {
+ client.clearHints();
+ });
+ }
+
+ filter(prefix: string): void {
+ this.broadcastToSlaves((client) => {
+ client.filterHints(prefix);
+ });
+ }
+
+ activate(tag: string): void {
+ this.followMasterRepository.clearTags();
+
+ let newTab = this.followMasterRepository.getCurrentNewTabMode();
+ let background = this.followMasterRepository.getCurrentBackgroundMode();
+ this.broadcastToSlaves((client) => {
+ client.activateIfExists(tag, newTab, background);
+ client.clearHints();
+ });
+ }
+
+ enqueue(key: string): void {
+ switch (key) {
+ case 'Enter':
+ this.activate(this.getCurrentTag());
+ return;
+ case 'Esc':
+ this.cancelFollow();
+ return;
+ case 'Backspace':
+ case 'Delete':
+ this.followKeyRepository.popKey();
+ this.filter(this.getCurrentTag());
+ return;
+ }
+
+ this.followKeyRepository.pushKey(key);
+
+ let tag = this.getCurrentTag();
+ let matched = this.followMasterRepository.getTagsByPrefix(tag);
+ if (matched.length === 0) {
+ this.cancelFollow();
+ } else if (matched.length === 1) {
+ this.activate(tag);
+ } else {
+ this.filter(tag);
+ }
+ }
+
+ private broadcastToSlaves(handler: (client: FollowSlaveClient) => void) {
+ let allFrames = [window.self].concat(Array.from(window.frames as any));
+ let clients = allFrames.map(frame => new FollowSlaveClientImpl(frame));
+ for (let client of clients) {
+ handler(client);
+ }
+ }
+
+ private getCurrentTag(): string {
+ return this.followKeyRepository.getKeys().join('');
+ }
+}
diff --git a/src/content/usecases/FollowSlaveUseCase.ts b/src/content/usecases/FollowSlaveUseCase.ts
new file mode 100644
index 0000000..eb011de
--- /dev/null
+++ b/src/content/usecases/FollowSlaveUseCase.ts
@@ -0,0 +1,91 @@
+import FollowSlaveRepository, { FollowSlaveRepositoryImpl }
+ from '../repositories/FollowSlaveRepository';
+import FollowPresenter, { FollowPresenterImpl }
+ from '../presenters/FollowPresenter';
+import TabsClient, { TabsClientImpl } from '../client/TabsClient';
+import { LinkHint, InputHint } from '../presenters/Hint';
+import FollowMasterClient, { FollowMasterClientImpl }
+ from '../client/FollowMasterClient';
+import Key from '../domains/Key';
+
+interface Size {
+ width: number;
+ height: number;
+}
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+export default class FollowSlaveUseCase {
+ private presenter: FollowPresenter;
+
+ private tabsClient: TabsClient;
+
+ private followMasterClient: FollowMasterClient;
+
+ private followSlaveRepository: FollowSlaveRepository;
+
+ constructor({
+ presenter = new FollowPresenterImpl(),
+ tabsClient = new TabsClientImpl(),
+ followMasterClient = new FollowMasterClientImpl(window.top),
+ followSlaveRepository = new FollowSlaveRepositoryImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ this.tabsClient = tabsClient;
+ this.followMasterClient = followMasterClient;
+ this.followSlaveRepository = followSlaveRepository;
+ }
+
+ countTargets(viewSize: Size, framePosition: Point): void {
+ let count = this.presenter.getTargetCount(viewSize, framePosition);
+ this.followMasterClient.responseHintCount(count);
+ }
+
+ createHints(viewSize: Size, framePosition: Point, tags: string[]): void {
+ this.followSlaveRepository.enableFollowMode();
+ this.presenter.createHints(viewSize, framePosition, tags);
+ }
+
+ showHints(prefix: string) {
+ this.presenter.filterHints(prefix);
+ }
+
+ sendKey(key: Key): void {
+ this.followMasterClient.sendKey(key);
+ }
+
+ isFollowMode(): boolean {
+ return this.followSlaveRepository.isFollowMode();
+ }
+
+ async activate(tag: string, newTab: boolean, background: boolean) {
+ let hint = this.presenter.getHint(tag);
+ if (!hint) {
+ return;
+ }
+
+ if (hint instanceof LinkHint) {
+ let url = hint.getLink();
+ // ignore taget='_blank'
+ if (!newTab && hint.getLinkTarget() === '_blank') {
+ hint.click();
+ return;
+ }
+ // eslint-disable-next-line no-script-url
+ if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) {
+ return;
+ }
+ await this.tabsClient.openUrl(url, newTab, background);
+ } else if (hint instanceof InputHint) {
+ hint.activate();
+ }
+ }
+
+ clear(): void {
+ this.followSlaveRepository.disableFollowMode();
+ this.presenter.clearHints();
+ }
+}
diff --git a/src/content/usecases/HintKeyProducer.ts b/src/content/usecases/HintKeyProducer.ts
new file mode 100644
index 0000000..241cd56
--- /dev/null
+++ b/src/content/usecases/HintKeyProducer.ts
@@ -0,0 +1,38 @@
+export default class HintKeyProducer {
+ private charset: string;
+
+ private counter: number[];
+
+ constructor(charset: string) {
+ if (charset.length === 0) {
+ throw new TypeError('charset is empty');
+ }
+
+ this.charset = charset;
+ this.counter = [];
+ }
+
+ produce(): string {
+ this.increment();
+
+ return this.counter.map(x => this.charset[x]).join('');
+ }
+
+ private increment(): void {
+ let max = this.charset.length - 1;
+ if (this.counter.every(x => x === max)) {
+ this.counter = new Array(this.counter.length + 1).fill(0);
+ return;
+ }
+
+ this.counter.reverse();
+ let len = this.charset.length;
+ let num = this.counter.reduce((x, y, index) => x + y * len ** index) + 1;
+ for (let i = 0; i < this.counter.length; ++i) {
+ this.counter[i] = num % len;
+ num = ~~(num / len);
+ }
+ this.counter.reverse();
+ }
+}
+
diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts
new file mode 100644
index 0000000..af0ad77
--- /dev/null
+++ b/src/content/usecases/KeymapUseCase.ts
@@ -0,0 +1,87 @@
+import KeymapRepository, { KeymapRepositoryImpl }
+ from '../repositories/KeymapRepository';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+import AddonEnabledRepository, { AddonEnabledRepositoryImpl }
+ from '../repositories/AddonEnabledRepository';
+
+import * as operations from '../../shared/operations';
+import { Keymaps } from '../../shared/Settings';
+import Key from '../domains/Key';
+import KeySequence, * as keySequenceUtils from '../domains/KeySequence';
+
+type KeymapEntityMap = Map<KeySequence, operations.Operation>;
+
+const reservedKeymaps: Keymaps = {
+ '<Esc>': { type: operations.CANCEL },
+ '<C-[>': { type: operations.CANCEL },
+};
+
+
+export default class KeymapUseCase {
+ private repository: KeymapRepository;
+
+ private settingRepository: SettingRepository;
+
+ private addonEnabledRepository: AddonEnabledRepository;
+
+ constructor({
+ repository = new KeymapRepositoryImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ addonEnabledRepository = new AddonEnabledRepositoryImpl(),
+ } = {}) {
+ this.repository = repository;
+ this.settingRepository = settingRepository;
+ this.addonEnabledRepository = addonEnabledRepository;
+ }
+
+ nextOp(key: Key): operations.Operation | null {
+ let sequence = this.repository.enqueueKey(key);
+
+ let keymaps = this.keymapEntityMap();
+ let matched = Array.from(keymaps.keys()).filter(
+ (mapping: KeySequence) => {
+ return mapping.startsWith(sequence);
+ });
+ if (!this.addonEnabledRepository.get()) {
+ // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
+ // the addon disabled
+ matched = matched.filter((keymap) => {
+ let type = (keymaps.get(keymap) as operations.Operation).type;
+ return type === operations.ADDON_ENABLE ||
+ type === operations.ADDON_TOGGLE_ENABLED;
+ });
+ }
+ if (matched.length === 0) {
+ // No operations to match with inputs
+ this.repository.clear();
+ return null;
+ } else if (matched.length > 1 ||
+ matched.length === 1 && sequence.length() < matched[0].length()) {
+ // More than one operations are matched
+ return null;
+ }
+ // Exactly one operation is matched
+ let operation = keymaps.get(matched[0]) as operations.Operation;
+ this.repository.clear();
+ return operation;
+ }
+
+ clear(): void {
+ this.repository.clear();
+ }
+
+ private keymapEntityMap(): KeymapEntityMap {
+ let keymaps = {
+ ...this.settingRepository.get().keymaps,
+ ...reservedKeymaps,
+ };
+ let entries = Object.entries(keymaps).map((entry) => {
+ return [
+ keySequenceUtils.fromMapKeys(entry[0]),
+ entry[1],
+ ];
+ }) as [KeySequence, operations.Operation][];
+ return new Map<KeySequence, operations.Operation>(entries);
+ }
+}
diff --git a/src/content/usecases/MarkKeyUseCase.ts b/src/content/usecases/MarkKeyUseCase.ts
new file mode 100644
index 0000000..c0aa655
--- /dev/null
+++ b/src/content/usecases/MarkKeyUseCase.ts
@@ -0,0 +1,36 @@
+import MarkKeyRepository, { MarkKeyRepositoryImpl }
+ from '../repositories/MarkKeyRepository';
+
+export default class MarkKeyUseCase {
+ private repository: MarkKeyRepository;
+
+ constructor({
+ repository = new MarkKeyRepositoryImpl()
+ } = {}) {
+ this.repository = repository;
+ }
+
+ isSetMode(): boolean {
+ return this.repository.isSetMode();
+ }
+
+ isJumpMode(): boolean {
+ return this.repository.isJumpMode();
+ }
+
+ enableSetMode(): void {
+ this.repository.enableSetMode();
+ }
+
+ disableSetMode(): void {
+ this.repository.disabeSetMode();
+ }
+
+ enableJumpMode(): void {
+ this.repository.enableJumpMode();
+ }
+
+ disableJumpMode(): void {
+ this.repository.disabeJumpMode();
+ }
+}
diff --git a/src/content/usecases/MarkUseCase.ts b/src/content/usecases/MarkUseCase.ts
new file mode 100644
index 0000000..530f141
--- /dev/null
+++ b/src/content/usecases/MarkUseCase.ts
@@ -0,0 +1,66 @@
+import ScrollPresenter, { ScrollPresenterImpl }
+ from '../presenters/ScrollPresenter';
+import MarkClient, { MarkClientImpl } from '../client/MarkClient';
+import MarkRepository, { MarkRepositoryImpl }
+ from '../repositories/MarkRepository';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient';
+
+export default class MarkUseCase {
+ private scrollPresenter: ScrollPresenter;
+
+ private client: MarkClient;
+
+ private repository: MarkRepository;
+
+ private settingRepository: SettingRepository;
+
+ private consoleClient: ConsoleClient;
+
+ constructor({
+ scrollPresenter = new ScrollPresenterImpl(),
+ client = new MarkClientImpl(),
+ repository = new MarkRepositoryImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ consoleClient = new ConsoleClientImpl(),
+ } = {}) {
+ this.scrollPresenter = scrollPresenter;
+ this.client = client;
+ this.repository = repository;
+ this.settingRepository = settingRepository;
+ this.consoleClient = consoleClient;
+ }
+
+ async set(key: string): Promise<void> {
+ let pos = this.scrollPresenter.getScroll();
+ if (this.globalKey(key)) {
+ this.client.setGloablMark(key, pos);
+ await this.consoleClient.info(`Set global mark to '${key}'`);
+ } else {
+ this.repository.set(key, pos);
+ await this.consoleClient.info(`Set local mark to '${key}'`);
+ }
+ }
+
+ async jump(key: string): Promise<void> {
+ if (this.globalKey(key)) {
+ await this.client.jumpGlobalMark(key);
+ } else {
+ let pos = this.repository.get(key);
+ if (!pos) {
+ throw new Error('Mark is not set');
+ }
+ this.scroll(pos.x, pos.y);
+ }
+ }
+
+ scroll(x: number, y: number): void {
+ let smooth = this.settingRepository.get().properties.smoothscroll;
+ this.scrollPresenter.scrollTo(x, y, smooth);
+ }
+
+ private globalKey(key: string) {
+ return (/^[A-Z0-9]$/).test(key);
+ }
+}
diff --git a/src/content/usecases/NavigateUseCase.ts b/src/content/usecases/NavigateUseCase.ts
new file mode 100644
index 0000000..6f82d3f
--- /dev/null
+++ b/src/content/usecases/NavigateUseCase.ts
@@ -0,0 +1,36 @@
+import NavigationPresenter, { NavigationPresenterImpl }
+ from '../presenters/NavigationPresenter';
+
+export default class NavigateUseCase {
+ private navigationPresenter: NavigationPresenter;
+
+ constructor({
+ navigationPresenter = new NavigationPresenterImpl(),
+ } = {}) {
+ this.navigationPresenter = navigationPresenter;
+ }
+
+ openHistoryPrev(): void {
+ this.navigationPresenter.openHistoryPrev();
+ }
+
+ openHistoryNext(): void {
+ this.navigationPresenter.openHistoryNext();
+ }
+
+ openLinkPrev(): void {
+ this.navigationPresenter.openLinkPrev();
+ }
+
+ openLinkNext(): void {
+ this.navigationPresenter.openLinkNext();
+ }
+
+ openParent(): void {
+ this.navigationPresenter.openParent();
+ }
+
+ openRoot(): void {
+ this.navigationPresenter.openRoot();
+ }
+}
diff --git a/src/content/usecases/ScrollUseCase.ts b/src/content/usecases/ScrollUseCase.ts
new file mode 100644
index 0000000..6a1f801
--- /dev/null
+++ b/src/content/usecases/ScrollUseCase.ts
@@ -0,0 +1,58 @@
+import ScrollPresenter, { ScrollPresenterImpl }
+ from '../presenters/ScrollPresenter';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+
+export default class ScrollUseCase {
+ private presenter: ScrollPresenter;
+
+ private settingRepository: SettingRepository;
+
+ constructor({
+ presenter = new ScrollPresenterImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ this.settingRepository = settingRepository;
+ }
+
+ scrollVertically(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollVertically(count, smooth);
+ }
+
+ scrollHorizonally(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollHorizonally(count, smooth);
+ }
+
+ scrollPages(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollPages(count, smooth);
+ }
+
+ scrollToTop(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToTop(smooth);
+ }
+
+ scrollToBottom(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToBottom(smooth);
+ }
+
+ scrollToHome(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToHome(smooth);
+ }
+
+ scrollToEnd(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToEnd(smooth);
+ }
+
+ private getSmoothScroll(): boolean {
+ let settings = this.settingRepository.get();
+ return settings.properties.smoothscroll;
+ }
+}
diff --git a/src/content/usecases/SettingUseCase.ts b/src/content/usecases/SettingUseCase.ts
new file mode 100644
index 0000000..765cb45
--- /dev/null
+++ b/src/content/usecases/SettingUseCase.ts
@@ -0,0 +1,24 @@
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+import SettingClient, { SettingClientImpl } from '../client/SettingClient';
+import Settings from '../../shared/Settings';
+
+export default class SettingUseCase {
+ private repository: SettingRepository;
+
+ private client: SettingClient;
+
+ constructor({
+ repository = new SettingRepositoryImpl(),
+ client = new SettingClientImpl(),
+ } = {}) {
+ this.repository = repository;
+ this.client = client;
+ }
+
+ async reload(): Promise<Settings> {
+ let settings = await this.client.load();
+ this.repository.set(settings);
+ return settings;
+ }
+}