-
-
{
- return Time.fromDate(date).toString();
- }}
- parseDate={(str) => {
- const time = Time.fromString(str);
- return time === undefined ? new Date() : time.toDate();
- }}
- closeOnSelection={false}
- value={startTime.toDate().toISOString()}
- onChange={handleDateChange}
- />
-
-
-
-
-
-
-
-
-
- {markers.map((marker) => (
-
-
-
- ))}
-
- {`${startTime.dateTime.month + 1}-${startTime.dateTime.day}`}
-
-
-
- {markers.map((marker) => (
-
-
-
- ))}
-
-
-
- {addIndicatorTime && (
-
-
-
- )}
-
-
-
addIndicatorTime !== undefined && addRecordAtTime()}
- />
- {recordItems.map(({ record, index }) => (
-
-
-
- ))}
-
-
-
-
- );
-}
diff --git a/src/renderer/data/data-model.ts b/src/renderer/data/data-model.ts
deleted file mode 100644
index 64bdd69..0000000
--- a/src/renderer/data/data-model.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-import Color from 'color';
-import { Time } from '../utils/time';
-import {
- MIN_ACTION_ID,
- MIN_ACTION_ID_INCLUDING_ROOT,
- ROOT_ACTION_ID,
-} from './data-state';
-
-export type ActionID = number;
-
-const SETTINGS_MARKER = '[SETTINGS]';
-
-const ACTIONS_MARKER = '[ACTIONS]';
-
-const RECORDS_MARKER = '[RECORDS]';
-
-enum ParsingMode {
- SETTINGS,
- ACTIONS,
- RECORDS,
-}
-
-export interface ActionModel {
- readonly id: ActionID;
- readonly name: string;
- readonly parentID?: ActionID;
- readonly color: Color;
-}
-
-export interface RecordModel {
- readonly time: Time;
- readonly actionID: ActionID;
- readonly duration: number;
-}
-
-export interface SettingsModel {
- readonly defaultRecordUnits: number;
- readonly hoursPerUnit: number;
-}
-
-export interface DataModel {
- readonly settings: SettingsModel;
- readonly actions: ActionModel[];
- readonly records: RecordModel[];
-}
-
-export const DEFAULT_SETTINGS: SettingsModel = {
- defaultRecordUnits: 1.0,
- hoursPerUnit: 1.0,
-};
-
-interface SettingsConfig
{
- parse(text: string): T;
- stringify(value: T): string;
-}
-
-const STRING_SETTING: SettingsConfig = {
- parse(text: string) {
- return text;
- },
- stringify(value: string): string {
- return value;
- },
-};
-
-const NUMBER_SETTING: SettingsConfig = {
- parse(text: string) {
- const duration = parseFloat(text);
- if (!Number.isFinite(duration) || duration < 0) {
- throw new Error(`Failed to parse number: ${text}`);
- }
- return duration;
- },
- stringify(value: number): string {
- return `${value}`;
- },
-};
-
-const SETTINGS_CONFIGS: {
- [Key in keyof SettingsModel]: SettingsConfig;
-} = {
- defaultRecordUnits: NUMBER_SETTING,
- hoursPerUnit: NUMBER_SETTING,
-};
-
-export class DataModelReader {
- read(text: string): DataModel {
- const model: DataModel = {
- settings: { ...DEFAULT_SETTINGS },
- actions: [],
- records: [],
- };
-
- const lines = text
- .split(/\r?\n/)
- .filter((line) => line.trimEnd().length > 0);
-
- let parsingMode = ParsingMode.SETTINGS;
-
- const parentStack: [ActionID, number][] = [];
-
- for (const line of lines) {
- if (line.startsWith('[')) {
- if (line === SETTINGS_MARKER) {
- parsingMode = ParsingMode.SETTINGS;
- } else if (line === ACTIONS_MARKER) {
- parsingMode = ParsingMode.ACTIONS;
- } else if (line === RECORDS_MARKER) {
- parsingMode = ParsingMode.RECORDS;
- }
- } else {
- if (parsingMode === ParsingMode.SETTINGS) {
- const colonIndex = line.indexOf(':');
- if (colonIndex === -1) {
- throw new Error(`Invalid settings entry: ${line}`);
- }
- const key = line.substring(0, colonIndex).trim();
- const valueText = line.substring(colonIndex + 1).trim();
- const config = SETTINGS_CONFIGS[key as any];
- if (config === undefined) {
- throw new Error(`Invalid settings key: ${key}`);
- }
- const value = config.parse(valueText);
- model.settings[key as any] = value;
- } else if (parsingMode === ParsingMode.ACTIONS) {
- let indentCount = 0;
- while (indentCount < line.length && line[indentCount] === ' ') {
- indentCount += 1;
- }
- while (
- parentStack.length > 0 &&
- indentCount <= parentStack[parentStack.length - 1][1]
- ) {
- parentStack.pop();
- }
- const tokens = line
- .substring(indentCount)
- .trim()
- .split(',')
- .map((token) => token.trim());
- const id = this.parseActionID(tokens[0]);
- if (tokens.length === 4) {
- // Legacy parsing mode
- const name = tokens[1];
- const parentID =
- tokens[2].length === 0
- ? undefined
- : this.parseParentActionID(tokens[2]);
- const color = Color(tokens[3]);
- model.actions.push({ id, name, parentID, color });
- } else {
- const name = tokens[1];
- const parentID =
- parentStack.length > 0
- ? parentStack[parentStack.length - 1][0]
- : 0;
- const color = Color(tokens[2]);
- model.actions.push({ id, name, parentID, color });
- }
- parentStack.push([id, indentCount]);
- } else if (parsingMode === ParsingMode.RECORDS) {
- const tokens = line
- .trim()
- .split(',')
- .map((token) => token.trim());
- const time = this.parseTime(tokens[0]);
- const actionID = this.parseActionID(tokens[1]);
- const duration = this.parseDuration(tokens[2]);
- model.records.push({ actionID, time, duration });
- }
- }
- }
-
- return model;
- }
-
- parseActionID(token: string) {
- const id = parseInt(token, 10);
- if (!Number.isFinite(id) || id < MIN_ACTION_ID) {
- throw new Error(`Invalid action ID: ${token}`);
- }
- return id;
- }
-
- parseParentActionID(token: string) {
- const id = parseInt(token, 10);
- if (!Number.isFinite(id) || id < MIN_ACTION_ID_INCLUDING_ROOT) {
- throw new Error(`Invalid parent action ID: ${token}`);
- }
- return id;
- }
-
- parseTime(token: string) {
- const time = Time.fromString(token);
- if (time === undefined) {
- throw new Error(`Invalid time: ${token}`);
- }
- return time;
- }
-
- parseDuration(token: string) {
- const duration = parseFloat(token);
- if (!Number.isFinite(duration) || duration < 0) {
- throw new Error(`Invalid duration: ${token}`);
- }
- return duration;
- }
-}
-
-export class DataModelWriter {
- write(model: DataModel): string {
- const lines: string[] = [];
-
- lines.push(SETTINGS_MARKER);
- const settingsKeys = Object.keys(model.settings);
- settingsKeys.sort();
- for (const key of settingsKeys) {
- const config = SETTINGS_CONFIGS[key as any];
- if (!config) continue;
- lines.push(`${key}: ${config.stringify(model.settings[key as any])}`);
- }
-
- lines.push('');
- lines.push(ACTIONS_MARKER);
-
- const children = new Map();
- const idToAction = new Map();
- for (const action of model.actions) {
- idToAction.set(action.id, action);
- if (action.parentID !== undefined) {
- if (children.has(action.parentID)) {
- children.get(action.parentID)?.push(action.id);
- } else {
- children.set(action.parentID, [action.id]);
- }
- }
- }
- const visited = new Set();
- const dfs = (id: ActionID, level: number) => {
- if (visited.has(id)) {
- throw new Error('Action list is not a tree');
- }
- visited.add(id);
- if (id !== ROOT_ACTION_ID) {
- const action = idToAction.get(id);
- if (action === undefined) {
- throw new Error('Undefined action');
- }
- lines.push(
- `${' '.repeat(level - 1)}${action.id},${
- action.name
- },${action.color.hex()}`
- );
- }
- for (const childID of children.get(id) || []) {
- dfs(childID, level + 1);
- }
- };
- dfs(ROOT_ACTION_ID, 0);
-
- lines.push('');
- lines.push(RECORDS_MARKER);
-
- for (const record of model.records) {
- lines.push(
- `${record.time.toString()},${record.actionID},${record.duration}`
- );
- }
-
- return lines.join('\n');
- }
-}
diff --git a/src/renderer/data/data-state.ts b/src/renderer/data/data-state.ts
deleted file mode 100644
index 6152261..0000000
--- a/src/renderer/data/data-state.ts
+++ /dev/null
@@ -1,491 +0,0 @@
-import produce, { Draft, immerable } from 'immer';
-import {
- ActionID,
- ActionModel,
- DataModel,
- DEFAULT_SETTINGS,
- RecordModel,
- SettingsModel,
-} from './data-model';
-import Color from 'color';
-import { clamp, firstGeq, firstLessThan, generateColor } from '../utils/utils';
-import { Time } from '../utils/time';
-
-export const ROOT_ACTION_ID: ActionID = 0;
-export const MIN_ACTION_ID: ActionID = 1;
-export const MIN_ACTION_ID_INCLUDING_ROOT: ActionID = 0;
-export const OTHER_CHILD_NAME = 'other';
-
-export const ILLEGAL_ACTION_NAME_CHARACTERS = /[:[,\]]/;
-
-export interface RecordReferenceSignature {}
-
-function newRecordReferenceSignature(): RecordReferenceSignature {
- return {};
-}
-
-export class Action {
- [immerable] = true;
-
- childIDs: ActionID[] = [];
-
- get isLeaf() {
- return this.childIDs.length === 0;
- }
-
- get isGroup() {
- return this.childIDs.length > 0;
- }
-
- get isRoot() {
- return this.model.id === ROOT_ACTION_ID;
- }
-
- constructor(public model: ActionModel) {}
-
- getCanonicalName(state: DataState, maxNumParts: number = -1) {
- if (this.isRoot) {
- return '';
- }
-
- const result: string[] = [this.model.name];
- let parentID = this.model.parentID;
- while (
- parentID !== undefined &&
- parentID !== ROOT_ACTION_ID &&
- (maxNumParts < 0 || result.length < maxNumParts)
- ) {
- const parent = state.actions.get(parentID);
- if (parent !== undefined) {
- result.push(parent.model.name);
- parentID = parent.model.parentID;
- } else {
- break;
- }
- }
- result.reverse();
- return result.join(': ');
- }
-}
-
-export class Record {
- [immerable] = true;
-
- constructor(public model: RecordModel) {}
-}
-
-export interface RecordWithIndex {
- index: number;
- record: Record;
-}
-
-export class DataState {
- [immerable] = true;
-
- readonly nextActionID: number;
-
- // This is an abstract object such that, if two data state has the same
- // recordReferenceSignature (referential equality), then it is valid to modify
- // a record at a certain index (e.g. using updateRecord) on both.
- // This is used by components to know when to throw away an index.
- readonly recordReferenceSignature = newRecordReferenceSignature();
-
- private constructor(
- public actions = new Map(),
- // Records are guaranteed to be sorted.
- public records: Record[] = [],
- public settings: SettingsModel = DEFAULT_SETTINGS,
- public filePath?: string
- ) {
- let nextID = 0;
- actions.forEach((action) => {
- nextID = Math.max(action.model.id, nextID);
- });
- nextID += 1;
- this.nextActionID = nextID;
- }
-
- get rootAction() {
- const result = this.actions.get(ROOT_ACTION_ID);
- if (result === undefined) {
- throw new Error('Root action undefined');
- }
- return result;
- }
-
- getRecordsInRange(startTime: Time, endTime: Time): RecordWithIndex[] {
- if (this.records.length === 0) {
- return [];
- }
- const startIndex = clamp(
- firstLessThan(
- this.records,
- startTime,
- (a, b) => a.lessThan(b),
- (record) => record.model.time
- ),
- 0,
- this.records.length - 1
- );
- const endIndex = clamp(
- firstGeq(
- this.records,
- endTime,
- (a, b) => a.lessThan(b),
- (record) => record.model.time
- ) + 1,
- startIndex,
- this.records.length
- );
- const result = [] as RecordWithIndex[];
- for (let i = startIndex; i < endIndex; ++i) {
- result.push({ record: this.records[i], index: i });
- }
- return result;
- }
-
- private sortRecord() {
- const sortedRecords = this.records.slice();
- sortedRecords.sort((a, b) => {
- return a.model.time.seconds - b.model.time.seconds;
- });
- // Sort records
- return produce(this, (draft) => {
- draft.records = sortedRecords;
- });
- }
-
- addRecord(model: RecordModel) {
- return produce(this, (draft) => {
- draft.records.push(new Record(model));
- draft.recordReferenceSignature = newRecordReferenceSignature();
- }).sortRecord();
- }
-
- updateRecord(index: number, model: RecordModel) {
- if (index < 0 || index >= this.records.length) {
- return this;
- }
- return produce(this, (draft) => {
- draft.records[index] = new Record(model);
- }).sortRecord();
- }
-
- removeRecord(index: number) {
- if (index < 0 || index >= this.records.length) {
- return this;
- }
- return produce(this, (draft) => {
- draft.records.splice(index, 1);
- draft.recordReferenceSignature = newRecordReferenceSignature();
- });
- }
-
- updateSettings(recipe: (draft: Draft) => void) {
- return produce(this, (draft) => {
- recipe(draft.settings);
- });
- }
-
- rescaleAllRecords(oldHoursPerUnit: number, newHoursPerUnit: number) {
- const factor = oldHoursPerUnit / newHoursPerUnit;
- if (!Number.isFinite(factor) || factor <= 0) {
- return this;
- }
- return produce(this, (draft) => {
- for (let i = 0; i < draft.records.length; ++i) {
- draft.records[i].model.duration *= factor;
- }
- });
- }
-
- // Note: This reads the current state, not draft.
- private makeDummyChild(
- actionID: number,
- parentID: number,
- model: Omit,
- draft: Draft,
- parent: Draft
- ) {
- // We want to move records on the parent to a dummy child since actions
- // with children cannot have records.
- let parentHasRecord = false;
- const numRecords = this.records.length;
- for (let i = 0; i < numRecords; ++i) {
- if (this.records[i].model.actionID === parentID) {
- parentHasRecord = true;
- break;
- }
- }
- if (parentHasRecord) {
- let otherChildID: ActionID;
- if (
- model.name.trim().toLowerCase() ===
- OTHER_CHILD_NAME.trim().toLowerCase()
- ) {
- otherChildID = actionID;
- } else {
- otherChildID = draft.nextActionID;
- draft.nextActionID += 1;
- parent.childIDs.push(otherChildID);
- draft.actions.set(
- otherChildID,
- new Action({
- color: generateColor(),
- name: OTHER_CHILD_NAME,
- parentID,
- id: otherChildID,
- })
- );
- }
-
- for (let i = 0; i < numRecords; ++i) {
- if (this.records[i].model.actionID === parentID) {
- draft.records[i].model.actionID = otherChildID;
- }
- }
- }
- }
-
- static validateActionName(name: string) {
- return (
- name === name.trim() &&
- name.length > 0 &&
- name.search(ILLEGAL_ACTION_NAME_CHARACTERS) === -1
- );
- }
-
- addAction(model: Omit) {
- if (!DataState.validateActionName(model.name)) {
- throw new Error(`Invalid action name: ${model.name}`);
- }
- const newID = this.nextActionID;
- return produce(this, (draft) => {
- // TODO validation
- draft.nextActionID += 1;
- if (model.parentID === undefined) {
- throw new Error('model.parentID is undefined');
- }
- const parentID = model.parentID;
- const parent = draft.actions.get(parentID);
- if (parent === undefined) {
- throw new Error('parent is undefined');
- }
- const parentIsLeaf = parent.isLeaf;
- parent.childIDs.push(newID);
- draft.actions.set(newID, new Action({ ...model, id: newID }));
- if (parentIsLeaf) {
- this.makeDummyChild(newID, parentID, model, draft, parent);
- }
- });
- }
-
- updateAction(model: ActionModel): DataState {
- if (!DataState.validateActionName(model.name)) {
- throw new Error(`Invalid action name: ${model.name}`);
- }
- const actionID = model.id;
- return produce(this, (draft) => {
- // TODO validation
- const oldAction = draft.actions.get(actionID);
- if (oldAction === undefined) {
- throw new Error('Old action undefined');
- }
- const oldParentID = oldAction.model.parentID;
- if (oldParentID === undefined) {
- throw new Error('Old parent ID is undefined');
- }
- const newParentID = model.parentID;
- if (newParentID === undefined) {
- throw new Error('newParentID is undefined');
- }
- const oldParent = draft.actions.get(oldParentID);
- if (oldParent === undefined) {
- throw new Error('Old parent is undefined');
- }
- oldAction.model = { ...model };
- if (newParentID !== oldParentID) {
- const newParent = draft.actions.get(newParentID);
- if (newParent === undefined) {
- throw new Error('newParent is undefined');
- }
- const parentIsLeaf = newParent.isLeaf;
- oldParent.childIDs.splice(oldParent.childIDs.indexOf(actionID), 1);
- newParent.childIDs.push(actionID);
- if (parentIsLeaf) {
- this.makeDummyChild(actionID, newParentID, model, draft, newParent);
- }
- }
- });
- }
-
- removeAction(id: ActionID) {
- const action = this.actions.get(id);
- if (action === undefined || action.isRoot) return this;
-
- const childrenID = [] as ActionID[];
- const childrenIDSet = new Set();
- const dfs = (a: Action) => {
- childrenID.push(a.model.id);
- childrenIDSet.add(a.model.id);
- for (const childID of a.childIDs) {
- const child = this.actions.get(childID);
- if (child !== undefined) {
- dfs(child);
- }
- }
- };
- dfs(action);
-
- return produce(this, (draft) => {
- const parent =
- action.model.parentID === undefined
- ? undefined
- : draft.actions.get(action.model.parentID);
- if (parent !== undefined) {
- const idx = parent.childIDs.indexOf(id);
- if (idx >= 0) {
- parent.childIDs.splice(idx, 1);
- }
- }
-
- for (const childID of childrenID) {
- draft.actions.delete(childID);
- }
-
- draft.records = this.records.filter(
- (record) => !childrenIDSet.has(record.model.actionID)
- );
- });
- }
-
- toModel() {
- const model: DataModel = {
- settings: this.settings,
- actions: [],
- records: [],
- };
-
- this.actions.forEach((action) => {
- // Do not include the root action.
- if (action.isRoot) return;
- model.actions.push(action.model);
- });
-
- this.records.forEach((record) => {
- model.records.push(record.model);
- });
-
- return model;
- }
-
- static fromModel(model: DataModel, filePath?: string) {
- const actions = new Map();
- const records: Record[] = [];
-
- actions.set(ROOT_ACTION_ID, this.createRootAction());
-
- for (const actionModel of model.actions) {
- if (actionModel.id < MIN_ACTION_ID) {
- throw new Error(
- `Action [${actionModel.name}] has invalid action ID ${actionModel.id}`
- );
- }
- if (actions.has(actionModel.id)) {
- throw new Error(
- `Action [${actionModel.name}] has action ID ${actionModel.id} which already exists`
- );
- }
- actions.set(actionModel.id, new Action(actionModel));
- }
- // Reconcile children information
- for (const actionModel of model.actions) {
- if (actionModel.parentID !== undefined) {
- const parent = actions.get(actionModel.parentID);
- if (parent === undefined) {
- throw new Error(
- `Action [${actionModel.name}] has invalid parent ID ${actionModel.parentID}`
- );
- }
- parent.childIDs.push(actionModel.id);
- }
- }
-
- if (!this.isTree(actions)) {
- throw new Error(`Actions is not a valid tree.`);
- }
-
- for (const recordModel of model.records) {
- if (recordModel.duration < 0) {
- throw new Error(
- `Record at ${recordModel.time} has invalid duration ${recordModel.duration}`
- );
- }
- if (!actions.has(recordModel.actionID)) {
- throw new Error(
- `Record at ${recordModel.time} has invalid action ID ${recordModel.actionID}`
- );
- }
- records.push(new Record(recordModel));
- }
-
- records.sort((a, b) => {
- return a.model.time.seconds - b.model.time.seconds;
- });
-
- return new DataState(actions, records, model.settings, filePath);
- }
-
- dfs(
- actionID: ActionID,
- fns: {
- preOrder?: (action: Action, level: number) => void;
- postOrder?: (action: Action, level: number) => void;
- },
- level = 0
- ) {
- const action = this.actions.get(actionID);
- if (action === undefined) return;
- fns.preOrder?.(action, level);
- for (const childID of action.childIDs) {
- this.dfs(childID, fns, level + 1);
- }
- fns.postOrder?.(action, level);
- }
-
- static createEmpty(filePath?: string) {
- return new DataState(
- new Map([[ROOT_ACTION_ID, this.createRootAction()]]),
- [],
- DEFAULT_SETTINGS,
- filePath
- );
- }
-
- private static createRootAction() {
- return new Action({
- id: ROOT_ACTION_ID,
- name: `All`,
- parentID: undefined,
- color: Color('#888888'),
- });
- }
-
- private static isTree(actions: Map) {
- const visited = new Set();
- const dfs = (action: Action) => {
- if (visited.has(action.model.id)) {
- return false;
- }
- visited.add(action.model.id);
- for (const childID of action.childIDs) {
- const child = actions.get(childID);
- if (child !== undefined) {
- if (!dfs(child)) return false;
- }
- }
- return true;
- };
- return dfs(actions.get(ROOT_ACTION_ID)!);
- }
-}
diff --git a/src/renderer/index.ejs b/src/renderer/index.ejs
deleted file mode 100644
index 3d760b3..0000000
--- a/src/renderer/index.ejs
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Rapid Time Tracker
-
-
-
-
-
diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx
deleted file mode 100644
index a96350e..0000000
--- a/src/renderer/index.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { createRoot } from 'react-dom/client';
-import App from './App';
-
-const container = document.getElementById('root')!;
-const root = createRoot(container);
-root.render();
-
-// calling IPC exposed from preload script
-window.electron.ipcRenderer.once('ipc-example', (arg) => {
- // eslint-disable-next-line no-console
- console.log(arg);
-});
-window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']);
diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts
deleted file mode 100644
index 222344e..0000000
--- a/src/renderer/preload.d.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Channels } from 'main/preload';
-import * as fs from 'fs';
-import * as path from 'path';
-import * as os from 'os';
-import { dialog } from 'electron';
-
-declare global {
- interface Window {
- electron: {
- ipcRenderer: {
- sendMessage(channel: Channels, args: unknown[]): void;
- on(
- channel: string,
- func: (...args: unknown[]) => void
- ): (() => void) | undefined;
- once(channel: string, func: (...args: unknown[]) => void): void;
- };
- fs: typeof fs;
- os: typeof os;
- path: typeof path;
- dialog: typeof dialog;
- };
- }
-}
-
-export {};
diff --git a/src/renderer/utils/VirtualScroller.module.css b/src/renderer/utils/VirtualScroller.module.css
deleted file mode 100644
index 1723eea..0000000
--- a/src/renderer/utils/VirtualScroller.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.container {
- width: 100%;
- height: 100%;
- overflow: hidden;
-}
diff --git a/src/renderer/utils/VirtualScroller.tsx b/src/renderer/utils/VirtualScroller.tsx
deleted file mode 100644
index 1a7930a..0000000
--- a/src/renderer/utils/VirtualScroller.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- ComponentType,
- useCallback,
- useEffect,
- useLayoutEffect,
- useRef,
- useState,
-} from 'react';
-import {
- FixedSizeList,
- ListChildComponentProps,
- ListItemKeySelector,
-} from 'react-window';
-import styles from './VirtualScroller.module.css';
-import { Notifier } from './utils';
-
-const INITIAL_HEIGHT = 500;
-
-export interface VirtualScrollerContext {
- itemDataList: TItemData[];
- sharedData: TSharedData;
-}
-
-export const VirtualScroller = ({
- initialHeight = INITIAL_HEIGHT,
- itemHeight,
- itemDataList,
- sharedData,
- itemKeyFn,
- children,
- focusIndex,
- updateHeightNotifier,
-}: {
- initialHeight?: number;
- itemHeight: number;
- itemDataList: TItemData[];
- sharedData: TSharedData;
- itemKeyFn?: ListItemKeySelector<
- VirtualScrollerContext
- >;
- children: ComponentType<
- ListChildComponentProps>
- >;
- focusIndex?: number;
- updateHeightNotifier?: Notifier;
-}) => {
- const containerRef = useRef(null);
-
- const [height, setHeight] = useState(initialHeight);
-
- const updateHeight = useCallback(() => {
- if (containerRef.current) {
- setHeight(containerRef.current.clientHeight);
- }
- }, []);
-
- useLayoutEffect(updateHeight, [updateHeightNotifier, updateHeight]);
-
- useEffect(() => {
- window.addEventListener('resize', updateHeight);
- return () => window.removeEventListener('resize', updateHeight);
- }, [updateHeight]);
-
- const scrollerRef = useRef(null);
-
- useEffect(() => {
- if (focusIndex !== undefined) {
- scrollerRef.current?.scrollToItem(focusIndex);
- }
- }, [focusIndex]);
-
- return (
-
-
- {children}
-
-
- );
-};
diff --git a/src/renderer/utils/electron.service.ts b/src/renderer/utils/electron.service.ts
deleted file mode 100644
index 8fcd61c..0000000
--- a/src/renderer/utils/electron.service.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// If you import a module but never use any of the imported values other than
-// as TypeScript types, the resulting javascript file will look as if you never
-// imported the module at all.
-import { dialog } from 'electron';
-import * as fs from 'fs';
-import * as os from 'os';
-import * as path from 'path';
-
-let global: ElectronService | undefined;
-
-export class ElectronService {
- dialog: typeof dialog;
- fs: typeof fs;
- os: typeof os;
- path: typeof path;
-
- constructor() {
- this.dialog = window.electron.dialog;
- this.fs = window.electron.fs;
- this.os = window.electron.os;
- this.path = window.electron.path;
- }
-
- static get current() {
- if (global === undefined) {
- global = new ElectronService();
- }
- return global;
- }
-}
diff --git a/src/renderer/utils/fs-util.ts b/src/renderer/utils/fs-util.ts
deleted file mode 100644
index a73057b..0000000
--- a/src/renderer/utils/fs-util.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { ElectronService } from './electron.service';
-
-export class FsUtil {
- private get els() {
- return ElectronService.current;
- }
-
- get path() {
- return this.els.path;
- }
-
- homeDir() {
- return this.els.os.homedir();
- }
-
- readDirPathSync(defaultPath?: string) {
- const p = this.els.dialog.showOpenDialogSync({
- defaultPath,
- properties: ['openDirectory'],
- });
- if (p === undefined) return undefined;
- return p[0];
- }
-
- readFilePath(defaultPath?: string) {
- return this.els.dialog.showOpenDialog({
- defaultPath,
- properties: ['openFile'],
- filters: [{ name: 'Data File', extensions: ['txt'] }],
- });
- }
-
- readNewFilePath(defaultPath?: string) {
- return this.els.dialog.showSaveDialog({
- defaultPath,
- properties: [],
- filters: [{ name: 'Data File', extensions: ['txt'] }],
- });
- }
-
- ensureParentDirExistsSync(filePath: string) {
- this.els.fs.mkdirSync(this.els.path.dirname(filePath), {
- recursive: true,
- });
- }
-
- readFileTextSync(filePath: string): string | undefined {
- if (this.els.fs.existsSync(filePath)) {
- return this.els.fs.readFileSync(filePath, { encoding: 'utf8' });
- }
- return undefined;
- }
-
- isPathExistSync(filePath: string) {
- return this.els.fs.existsSync(filePath);
- }
-
- safeWriteFileSync(
- filePath: string,
- text: string,
- tempFilePath1: string,
- tempFilePath2: string
- ) {
- this.ensureParentDirExistsSync(filePath);
- if (this.els.fs.existsSync(filePath)) {
- // const dir = this.els.path.dirname(filePath);
- if (this.els.fs.existsSync(tempFilePath1)) {
- alert(
- `Unable to write file: temp file (${tempFilePath1}) already exists`
- );
- }
- if (this.els.fs.existsSync(tempFilePath2)) {
- alert(
- `Unable to write file: temp file (${tempFilePath2}) already exists`
- );
- }
- this.els.fs.writeFileSync(tempFilePath1, text);
- this.els.fs.renameSync(filePath, tempFilePath2);
- this.els.fs.renameSync(tempFilePath1, filePath);
- this.els.fs.unlinkSync(tempFilePath2);
- } else {
- this.els.fs.writeFileSync(filePath, text);
- }
- }
-}
diff --git a/src/renderer/utils/time.ts b/src/renderer/utils/time.ts
deleted file mode 100644
index cdd08d0..0000000
--- a/src/renderer/utils/time.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import { padNum } from './utils';
-
-export class DateTime {
- constructor(
- public year: number = 1970,
- public month: number = 0,
- public day: number = 1,
- public hours: number = 0,
- public minutes: number = 0,
- public seconds: number = 0,
- public dayOfWeek: number = 0
- ) {}
-}
-
-export class Time {
- public static matchPattern = /\b\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\b/;
- private static parsePattern =
- /\b(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\b/;
- private static parseDatePattern = /\b(\d\d\d\d)-(\d\d)-(\d\d)\b/;
-
- // seconds since epoch
- private _seconds: number = 0;
-
- public get seconds(): number {
- return this._seconds;
- }
-
- private _dateTime: DateTime | undefined = undefined;
-
- public get dateTime(): DateTime {
- if (!this._dateTime) {
- let date: Date = new Date(this.seconds * 1000);
- this._dateTime = {
- year: date.getUTCFullYear(),
- month: date.getUTCMonth(),
- day: date.getUTCDate(),
- hours: date.getUTCHours(),
- minutes: date.getUTCMinutes(),
- seconds: date.getUTCSeconds(),
- dayOfWeek: date.getUTCDay(),
- };
- }
- return this._dateTime;
- }
-
- public constructor(seconds: number = 0) {
- this._seconds = seconds;
- }
-
- public timeTo(time: Time): Time {
- return new Time(this.secondsTo(time));
- }
-
- public secondsTo(time: Time): number {
- return time.seconds - this.seconds;
- }
-
- public minutesTo(time: Time): number {
- return (time.seconds - this.seconds) / 60;
- }
-
- public get minutes() {
- return this.seconds / 60;
- }
-
- public get hours() {
- return this.seconds / 3600;
- }
-
- public getMinutesText() {
- let minutes = Math.floor(this.minutes);
- const hours = Math.floor(minutes / 60);
- minutes -= hours * 60;
- if (hours <= 0) {
- return `${minutes}m`;
- } else {
- return `${hours}h ${minutes}m`;
- }
- }
-
- public startOfWeek() {
- return this.addDays(-this.dateTime.dayOfWeek);
- }
-
- public startOfMondayWeek() {
- let dayOfWeek = this.dateTime.dayOfWeek - 1;
- if (dayOfWeek < 0) dayOfWeek += 7;
- return this.addDays(-dayOfWeek);
- }
-
- public startOfMonth() {
- return this.addDays(-(this.dateTime.day - 1));
- }
-
- public startOfYear() {
- const dateTime = this.dateTime;
- return Time.fromParams(
- dateTime.year,
- 0,
- 1,
- dateTime.hours,
- dateTime.minutes,
- dateTime.seconds
- );
- }
-
- public hoursTo(time: Time): number {
- return (time.seconds - this.seconds) / 3600;
- }
-
- public daysTo(time: Time): number {
- return (time.seconds - this.seconds) / 86400;
- }
-
- public add(time: Time): Time {
- return new Time(this.seconds + time.seconds);
- }
-
- public addSeconds(seconds: number): Time {
- return new Time(this.seconds + seconds);
- }
-
- public addMinutes(minutes: number): Time {
- return new Time(this.seconds + minutes * 60);
- }
-
- public addHours(hours: number): Time {
- return new Time(this.seconds + hours * 3600);
- }
-
- public addDays(days: number): Time {
- return new Time(this.seconds + days * 86400);
- }
-
- public addMonths(months: number): Time {
- const dateTime = this.dateTime;
- return Time.fromParams(
- dateTime.year,
- dateTime.month + months,
- dateTime.day,
- dateTime.hours,
- dateTime.minutes,
- dateTime.seconds
- );
- }
-
- public addYears(years: number): Time {
- const dateTime = this.dateTime;
- return Time.fromParams(
- dateTime.year + years,
- dateTime.month,
- dateTime.day,
- dateTime.hours,
- dateTime.minutes,
- dateTime.seconds
- );
- }
-
- public static now(): Time {
- return Time.fromDate(new Date());
- }
-
- public static nowMinute(): Time {
- return Time.fromDate(new Date()).floorTo(60);
- }
-
- public static epoch(): Time {
- return new Time(0);
- }
-
- public isEpoch(): boolean {
- return this.seconds === 0;
- }
-
- public static fromDate(date: Date): Time {
- let seconds =
- Date.UTC(
- date.getFullYear(),
- date.getMonth(),
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds()
- ) / 1000;
- return new Time(seconds);
- }
-
- public toDate() {
- const dateTime = this.dateTime;
- return new Date(
- dateTime.year,
- dateTime.month,
- dateTime.day,
- dateTime.hours,
- dateTime.minutes,
- dateTime.seconds
- );
- }
-
- public static fromDateTime(dateTime: DateTime): Time {
- let seconds =
- Date.UTC(
- dateTime.year,
- dateTime.month,
- dateTime.day,
- dateTime.hours,
- dateTime.minutes,
- dateTime.seconds
- ) / 1000;
- return new Time(seconds);
- }
-
- public static fromParams(
- year: number = 1970,
- month: number = 0,
- day: number = 1,
- hours: number = 0,
- minutes: number = 0,
- seconds: number = 0
- ): Time {
- return new Time(Date.UTC(year, month, day, hours, minutes, seconds) / 1000);
- }
-
- public static compare(a: Time, b: Time) {
- return a.seconds - b.seconds;
- }
-
- public static lessThan(a: Time, b: Time): boolean {
- return a.seconds < b.seconds;
- }
-
- public roundTo(interval: number): Time {
- return new Time(Math.round(this.seconds / interval) * interval);
- }
-
- public floorToDays() {
- return this.floorTo(86400);
- }
-
- public floorToHours() {
- return this.floorTo(3600);
- }
-
- public roundToDays() {
- return this.roundTo(86400);
- }
-
- public roundToHours() {
- return this.roundTo(3600);
- }
-
- public floorTo(interval: number): Time {
- return new Time(Math.floor(this.seconds / interval) * interval);
- }
-
- public ceilTo(interval: number): Time {
- return new Time(Math.ceil(this.seconds / interval) * interval);
- }
-
- public equals(time: Time): boolean {
- return this.seconds === time.seconds;
- }
-
- public lessThan(time: Time): boolean {
- return this.seconds < time.seconds;
- }
-
- public leq(time: Time): boolean {
- return this.seconds <= time.seconds;
- }
-
- public greaterThan(time: Time): boolean {
- return this.seconds > time.seconds;
- }
-
- public geq(time: Time): boolean {
- return this.seconds >= time.seconds;
- }
-
- public static min(a: Time, b: Time): Time {
- return a.lessThan(b) ? a : b;
- }
-
- public static max(a: Time, b: Time): Time {
- return a.greaterThan(b) ? a : b;
- }
-
- public toString(): string {
- return `${padNum(this.dateTime.year, 4)}-${padNum(
- this.dateTime.month + 1,
- 2
- )}-${padNum(this.dateTime.day, 2)} ${padNum(
- this.dateTime.hours,
- 2
- )}:${padNum(this.dateTime.minutes, 2)}:${padNum(this.dateTime.seconds, 2)}`;
- }
-
- public static fromString(text: string) {
- const m = text.match(Time.parsePattern);
- if (!m) {
- return undefined;
- }
- return Time.fromParams(
- parseInt(m[1], 10),
- parseInt(m[2], 10) - 1,
- parseInt(m[3], 10),
- parseInt(m[4], 10),
- parseInt(m[5], 10),
- parseInt(m[6], 10)
- );
- }
-
- public toDateString(): string {
- return `${padNum(this.dateTime.year, 4)}-${padNum(
- this.dateTime.month + 1,
- 2
- )}-${padNum(this.dateTime.day, 2)}`;
- }
-
- public toMMString(): string {
- return `${padNum(this.dateTime.month + 1, 2)}`;
- }
-
- public toMMDDString(): string {
- return `${padNum(this.dateTime.month + 1, 2)}-${padNum(
- this.dateTime.day,
- 2
- )}`;
- }
-
- public toYYYYMMString() {
- return `${padNum(this.dateTime.year, 4)}-${padNum(
- this.dateTime.month + 1,
- 2
- )}`;
- }
-
- public toYYYYString() {
- return `${padNum(this.dateTime.year, 4)}`;
- }
-
- public static fromDateString(text: string) {
- const m = text.match(Time.parseDatePattern);
- if (!m) {
- return undefined;
- }
- return Time.fromParams(
- parseInt(m[1], 10),
- parseInt(m[2], 10) - 1,
- parseInt(m[3], 10),
- 0,
- 0,
- 0
- );
- }
-
- public static fromStringOrDefault(text: string, defaultTime: Time) {
- const time = this.fromString(text);
- if (time === undefined) return defaultTime;
- return time;
- }
-}
diff --git a/src/renderer/utils/utils.ts b/src/renderer/utils/utils.ts
deleted file mode 100644
index a58170d..0000000
--- a/src/renderer/utils/utils.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import convert from 'color-convert';
-import Color from 'color';
-import { useCallback, useEffect, useReducer } from 'react';
-
-export function padNum(num: number, digits: number) {
- return num.toString().padStart(digits, '0');
-}
-
-export function lerp(a: number, b: number, x: number) {
- return a + (b - a) * x;
-}
-
-export function linearMap(
- inStart: number,
- inEnd: number,
- outStart: number,
- outEnd: number,
- value: number
-) {
- return (
- outStart + ((value - inStart) * (outEnd - outStart)) / (inEnd - inStart)
- );
-}
-
-export function clampedLinearMap(
- inStart: number,
- inEnd: number,
- outStart: number,
- outEnd: number,
- value: number
-) {
- return Math.min(
- Math.max(linearMap(inStart, inEnd, outStart, outEnd, value), outStart),
- outEnd
- );
-}
-
-export function clamp(x: number, min: number, max: number) {
- return Math.min(Math.max(x, min), max);
-}
-
-export function random(min: number, max: number) {
- return min + Math.random() * (max - min);
-}
-
-export function generateColor() {
- const [r, g, b] = convert.hsl.rgb([
- random(0, 360),
- random(50, 100),
- random(50, 75),
- ]);
- return Color.rgb(r, g, b);
-}
-
-export function firstGeq(
- array: T[],
- value: K,
- lessThan: (a: K, b: K) => boolean,
- keyAt: (item: T) => K
-): number {
- let i: number;
- let first = 0;
- let step: number;
- let count: number = array.length;
- while (count > 0) {
- step = Math.floor(count / 2);
- i = first + step;
- if (lessThan(keyAt(array[i]), value)) {
- first = i + 1;
- count -= step + 1;
- } else count = step;
- }
- return first;
-}
-
-export function firstLessThan(
- array: T[],
- value: K,
- lessThan: (a: K, b: K) => boolean,
- keyAt: (item: T) => K
-): number {
- return firstGeq(array, value, lessThan, keyAt) - 1;
-}
-
-export class Counter {
- private data = new Map();
-
- add(key: K, amount: number) {
- this.data.set(key, (this.data.get(key) || 0) + amount);
- }
-
- get(key: K) {
- return this.data.get(key) || 0;
- }
-
- clear() {
- this.data = new Map();
- }
-
- copyFrom(other: Counter) {
- this.data.clear();
- other.data.forEach((count, key) => {
- this.data.set(key, count);
- });
- }
-}
-
-export function isOverlayOpen() {
- return document.body.classList.contains('bp4-overlay-open');
-}
-
-export function useGlobalShortcutKey(key: string, callback: () => void) {
- useEffect(() => {
- const fn = (e: KeyboardEvent) => {
- if (e.target !== document.body) return;
- if (isOverlayOpen()) return;
- if (e.key === key) {
- callback();
- e.preventDefault();
- }
- };
- document.body.addEventListener('keydown', fn);
- return () => document.body.removeEventListener('keydown', fn);
- }, [key, callback]);
-}
-
-export type Notifier = number;
-const INITIAL_NOTIFIER: Notifier = 0;
-
-export function useNotifier() {
- const [notifier, dispatch] = useReducer((s: Notifier, a: number) => {
- return s + a;
- }, INITIAL_NOTIFIER);
- const notify = useCallback(() => {
- dispatch(1);
- }, [dispatch]);
- return [notifier, notify] as [Notifier, () => void];
-}
-
-export function prettyNumber(
- x: number,
- isPercentage = false,
- maxFractionalDigits = 1
-) {
- let n = x;
- if (isPercentage) {
- n *= 100;
- }
- let result = n.toFixed(maxFractionalDigits);
- if (result.endsWith('0')) result = result.substring(0, result.length - 2);
- return isPercentage ? `${result}%` : result;
-}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index cde53f2..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "compilerOptions": {
- "incremental": true,
- "target": "es2021",
- "module": "commonjs",
- "lib": ["dom", "es2021"],
- "declaration": true,
- "declarationMap": true,
- "jsx": "react-jsx",
- "strict": true,
- "pretty": true,
- "sourceMap": true,
- "baseUrl": "./src",
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true,
- "moduleResolution": "node",
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "resolveJsonModule": true,
- "allowJs": true,
- "outDir": "release/app/dist",
- "strictNullChecks": true,
- "strictPropertyInitialization": true
- },
- "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"]
-}