diff --git a/README.md b/README.md index 561d482..d0d1c75 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ All these settings can be changed inside `settings.json`, they will be applied i - `daemon`: Where the LittleBigMouse_Daemon.exe file is located on your system - `interval`: The interval in milliseconds between each time the process will check the name of your current focused window, default is 1000ms +- `debounce`: The debounced time before turning on/off LBM when Alt+Tab-ing from a blacklisted application, default is 10000ms - `startup`: If this program should launch at Windows startup, default is true - `blacklist`: The list of programs that should turn OFF LBM, feel free to add your own diff --git a/client/src/components/App/App.tsx b/client/src/components/App/App.tsx index b59b074..4bae7b3 100644 --- a/client/src/components/App/App.tsx +++ b/client/src/components/App/App.tsx @@ -5,7 +5,6 @@ import { Container } from "typedi"; import { ActiveWindow } from "../../../../server/src/types/Common"; import { SocketHandler } from "../../managers/SocketHandler"; -import { bottomScroller } from "../../utils/HTMLUtils"; import { DebuggerRef } from "../Debugger/constants"; import { Debugger } from "../Debugger/Debugger"; @@ -24,7 +23,6 @@ const App = (props: AppProps) => { const [logs, setLogs] = useState([]); const [activeWindow, setActiveWindow] = useState(); - const $console = useRef(null); const $debugger = useRef(null); const socketHandler = Container.get(SocketHandler); @@ -34,9 +32,22 @@ const App = (props: AppProps) => { if (string[string.length - 1] !== "\n") { string += "\n"; } - setLogs((prevState) => prevState.concat(string)); + setLogs((prevState) => [string].concat(prevState)); }, []); + /* + useEffect(() => { + let count = 0; + const handler = setInterval(() => { + setLogs((prevState) => [count % 2 === 0 ? "Tic" : "Tac"].concat(prevState)); + count++; + }, 1000); + return () => { + clearInterval(handler); + }; + }, []); + */ + const handleActiveWindowChange = useCallback((data) => { setActiveWindow(data); }, []); @@ -68,17 +79,10 @@ const App = (props: AppProps) => { }; }, [handleNewMessages, handleActiveWindowChange, socketHandler]); - useEffect(() => { - const unsubscribe = bottomScroller($console.current); - return () => { - unsubscribe(); - }; - }, []); - return (
-
+        
           {logs.map((log, i) => (
             
               {log}
diff --git a/client/src/components/App/styles.ts b/client/src/components/App/styles.ts
index 020fc91..118ac9c 100644
--- a/client/src/components/App/styles.ts
+++ b/client/src/components/App/styles.ts
@@ -7,23 +7,21 @@ export const styles = {
   consoleContainer: {
     display: "flex",
     flex: 1,
-    overflow: "hidden",
-    flexFlow: "column",
+    background: "black",
+    overflowY: "auto",
+    flexFlow: "column-reverse",
   },
   console: {
     display: "flex",
-    flex: 1,
-    flexFlow: "column",
-    overflow: "auto",
-    background: "black",
+    flexFlow: "column-reverse",
     color: "white",
     fontFamily: "monospace",
+    marginBottom: "auto",
   },
   item: {
     //
   },
   activeWindow: {
-    display: "none",
     flexFlow: "column",
     background: "red",
     color: "white",
diff --git a/client/src/utils/HTMLUtils.tsx b/client/src/utils/HTMLUtils.tsx
index 25e0906..b4a476f 100644
--- a/client/src/utils/HTMLUtils.tsx
+++ b/client/src/utils/HTMLUtils.tsx
@@ -1,40 +1,68 @@
 function getScrollBottom(target: HTMLElement) {
-  const { height: rectHeight } = target.getBoundingClientRect();
-  const scrollHeight = target.scrollHeight + (Math.floor(rectHeight) - rectHeight);
-  return Math.max(0, scrollHeight - (rectHeight + target.scrollTop));
+  const { height: containerHeight } = target.getBoundingClientRect();
+  const scrollHeight = target.scrollHeight + (Math.floor(containerHeight) - containerHeight);
+  return Math.max(0, scrollHeight - (containerHeight + target.scrollTop));
 }
 
-export function bottomScroller(target: HTMLElement | null) {
-  if (!target) return () => undefined;
-
+export function bottomScroller(target: HTMLElement) {
+  let preventScrollHandling = false;
   let scrollBottomSaved = getScrollBottom(target);
-  let preventScrollComputation = false;
-  let timeoutHandler: NodeJS.Timer;
+
+  function handleWheel() {
+    scrollBottomSaved = getScrollBottom(target);
+  }
 
   function handleScroll() {
-    if (!target) return;
-    if (!preventScrollComputation) scrollBottomSaved = getScrollBottom(target);
+    if (target.scrollTop <= 1) target.scrollTop = 1;
+    if (preventScrollHandling) {
+      preventScrollHandling = false;
+    } else {
+      scrollBottomSaved = getScrollBottom(target);
+    }
   }
 
   function handleResize() {
-    if (!target) return;
-    preventScrollComputation = true;
-    const { height: rectHeight } = target.getBoundingClientRect();
-    const scrollHeight = target.scrollHeight;
-    target.scrollTop = scrollHeight - (rectHeight + scrollBottomSaved) + (scrollBottomSaved <= 1 ? 1 : 0);
-    clearTimeout(timeoutHandler);
-    timeoutHandler = setTimeout(() => {
-      preventScrollComputation = false;
-    }, 100);
+    preventScrollHandling = true;
+    const { height: containerHeight } = target.getBoundingClientRect();
+    const scrollHeight = target.scrollHeight + (Math.floor(containerHeight) - containerHeight);
+    target.scrollTop = scrollHeight - (containerHeight + scrollBottomSaved) + (scrollBottomSaved <= 1 ? 1 : 0); // This will trigger a scroll event
   }
 
-  target.addEventListener("scroll", handleScroll);
-
   const ro = new ResizeObserver((entries) => entries.forEach(handleResize));
   ro.observe(target);
 
+  target.addEventListener("scroll", handleScroll);
+  target.addEventListener("wheel", handleWheel);
+
   return () => {
-    target.removeEventListener("scroll", handleScroll);
     ro.unobserve(target);
+
+    target.removeEventListener("scroll", handleScroll);
+    target.removeEventListener("wheel", handleWheel);
+  };
+}
+
+export function autoScrollWhenScrollable(target: HTMLElement) {
+  const targetChild = target.children[0] as HTMLElement;
+
+  let parentHeight = target.offsetHeight;
+  let childHeight = targetChild.offsetHeight;
+  let scrollable = childHeight > parentHeight;
+
+  function handleResize() {
+    parentHeight = target.offsetHeight;
+    childHeight = targetChild.offsetHeight;
+    const nextScrollable = childHeight > parentHeight;
+    if (nextScrollable && !scrollable) {
+      target.scrollTop = 999999;
+    }
+    scrollable = nextScrollable;
+  }
+
+  const ro = new ResizeObserver((entries) => entries.forEach(handleResize));
+  ro.observe(targetChild);
+
+  return () => {
+    ro.unobserve(targetChild);
   };
 }
diff --git a/client/src/utils/ReactUtils.tsx b/client/src/utils/ReactUtils.tsx
index 042f663..ba84925 100644
--- a/client/src/utils/ReactUtils.tsx
+++ b/client/src/utils/ReactUtils.tsx
@@ -1,8 +1,7 @@
 import { useCallback, useState } from "react";
 
 export function useForceUpdate(): () => void {
-  // eslint-disable-next-line @typescript-eslint/ban-types
-  const [, dispatch] = useState<{}>(Object.create(null));
+  const [, dispatch] = useState(Object.create(null));
 
   // Turn dispatch(required_parameter) into dispatch().
   const memoizedDispatch = useCallback((): void => {
diff --git a/package-lock.json b/package-lock.json
index 05c01ca..c72370f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2824,6 +2824,12 @@
               "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
               "dev": true
             },
+            "p-cancelable": {
+              "version": "0.4.1",
+              "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz",
+              "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==",
+              "dev": true
+            },
             "pify": {
               "version": "3.0.0",
               "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@@ -6304,10 +6310,9 @@
       "dev": true
     },
     "p-cancelable": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz",
-      "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==",
-      "dev": true
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+      "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
     },
     "p-event": {
       "version": "2.3.1",
diff --git a/package.json b/package.json
index 458c43c..714a76b 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "postportable": "copyfiles *.vbs settings.json locales/* assets/* public/**/* node_modules/**/*.node node_modules/**/*.exe portable && npm run pack",
     "dev": "concurrently \"npm run client-dev\" \"npm run server-dev\"",
     "build": "npm run executable -- -o LBMM.exe",
+    "build-all": "concurrently \"npm run build\" \"npm run portable\"",
     "clean": "rimraf LBMM.exe portable dist LBMM.zip logs.txt",
     "lint": "eslint --ext=.jsx,.js,.tsx,.ts server,client --quiet --fix",
     "postinstall": "patch-package",
diff --git a/server/src/index.ts b/server/src/index.ts
index 1d4d6ef..fc470ce 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -3,7 +3,7 @@ import "reflect-metadata";
 import { Container } from "typedi";
 
 import { LittleBigMouseManager } from "./managers/LittleBigMouseManager";
-import { instancesRunning } from "./utils/processes";
+import { instancesRunning } from "./utils/ProcessesUtils";
 
 if (instancesRunning("LBMM.exe") > 1) {
   dialog.show({
diff --git a/server/src/managers/LittleBigMouseManager.ts b/server/src/managers/LittleBigMouseManager.ts
index 6251c66..8117e79 100644
--- a/server/src/managers/LittleBigMouseManager.ts
+++ b/server/src/managers/LittleBigMouseManager.ts
@@ -6,7 +6,8 @@ import { Service } from "typedi";
 import { Configuration } from "../settings/Configuration";
 import { Logger } from "../settings/Logger";
 import { Settings, SettingsChangeCallback } from "../settings/Settings";
-import { isRunning, runCommandSync } from "../utils/processes";
+import { sleep } from "../utils/JavascriptUtils";
+import { isRunning, runCommandSync } from "../utils/ProcessesUtils";
 
 import { ActiveWindowListener, ActiveWindowListenerCallback } from "./ActiveWindowListener";
 import { MainDisplayManager } from "./MainDisplayManager";
@@ -19,6 +20,7 @@ export class LittleBigMouseManager {
   private isLBMActive = true;
   private shouldIgnoreNextCalls = false;
   private callQueue: PQueue;
+  private canceller?: () => boolean;
 
   constructor(
     private logger: Logger,
@@ -65,51 +67,51 @@ export class LittleBigMouseManager {
         `Focused window (blacklisted=${activeWindow.isBlacklisted ? "true" : "false"}): ${activeWindowName}`,
       );
     }
-    this.callQueue.clear();
-    if (activeWindow && !activeWindow.isBlacklisted) {
-      this.callQueue.add(async () => await this.turnOnLBM());
-    } else {
-      this.callQueue.add(async () => await this.turnOffLBM());
-    }
-  };
-
-  private async changeLBMStatus(active: boolean) {
-    if (this.isLBMRunning()) {
-      runCommandSync(this.settings.daemon, [active ? "--start" : "--stop"]);
-      if (active) {
-        this.logger.log("LBM switched on");
+    const hasCancelled = this.canceller?.(); // Cancel any pending promises
+    this.callQueue.clear(); // Reduce queue to 0
+    if (!hasCancelled) {
+      if (activeWindow && !activeWindow.isBlacklisted) {
+        this.callQueue.add(async () => await this.updateLBMStatus(true));
       } else {
-        this.logger.log("LBM switched off");
+        this.callQueue.add(async () => await this.updateLBMStatus(false));
       }
-    } else {
-      this.callQueue.pause();
-      this.checkRunning();
     }
-  }
+  };
 
-  private async turnOnLBM() {
-    if (this.isLBMActive) {
+  private async updateLBMStatus(nextActive: boolean) {
+    if (this.isLBMActive === nextActive) {
       if (!this.shouldIgnoreNextCalls) {
-        this.logger.log("LBM is already on, skipping and next logs will be muted...");
+        this.logger.log(`LBM is already ${nextActive ? "on" : "off"}, skipping and next logs will be muted...`);
         this.shouldIgnoreNextCalls = true;
       }
     } else {
       this.shouldIgnoreNextCalls = false;
-      this.isLBMActive = true;
-      await this.changeLBMStatus(true);
-    }
-  }
-
-  private async turnOffLBM() {
-    if (!this.isLBMActive) {
-      if (!this.shouldIgnoreNextCalls) {
-        this.logger.log("LBM is already off, skipping and next logs will be muted...");
-        this.shouldIgnoreNextCalls = true;
+      if (this.isLBMRunning()) {
+        if (nextActive) {
+          try {
+            this.logger.log(`Waiting ${this.settings.debounce} to turn on`);
+            await sleep(this.settings.debounce, (cancel) => (this.canceller = cancel));
+            runCommandSync(this.settings.daemon, ["--start"]);
+            this.isLBMActive = true;
+            this.logger.log(`LBM switched on`);
+          } catch (e) {
+            this.logger.log(`Cancelling switch-on due to debounce`);
+          }
+        } else {
+          try {
+            this.logger.log(`Waiting ${this.settings.debounce} to turn off`);
+            await sleep(this.settings.debounce, (cancel) => (this.canceller = cancel));
+            runCommandSync(this.settings.daemon, ["--stop"]);
+            this.isLBMActive = false;
+            this.logger.log(`LBM switched off`);
+          } catch (e) {
+            this.logger.log(`Cancelling switch-off due to debounce`);
+          }
+        }
+      } else {
+        this.callQueue.pause();
+        this.checkRunning();
       }
-    } else {
-      this.shouldIgnoreNextCalls = false;
-      this.isLBMActive = false;
-      await this.changeLBMStatus(false);
     }
   }
 }
diff --git a/server/src/managers/TrayManager.ts b/server/src/managers/TrayManager.ts
index 1894041..2c24580 100644
--- a/server/src/managers/TrayManager.ts
+++ b/server/src/managers/TrayManager.ts
@@ -7,7 +7,7 @@ import { Service } from "typedi";
 import { I18n } from "../settings/I18n";
 import { Logger, pathToLogs } from "../settings/Logger";
 import { pathToSettingsJson, Settings } from "../settings/Settings";
-import { runCommandSync } from "../utils/processes";
+import { runCommandSync } from "../utils/ProcessesUtils";
 
 import { MainDisplayManager } from "./MainDisplayManager";
 
diff --git a/server/src/settings/Settings.ts b/server/src/settings/Settings.ts
index 033f024..99be2dc 100644
--- a/server/src/settings/Settings.ts
+++ b/server/src/settings/Settings.ts
@@ -14,6 +14,7 @@ export const pathToSettingsJson = path.resolve(process.cwd(), "settings.json");
 interface SettingsMap {
   daemon: string;
   interval: number;
+  debounce: number;
   startup: boolean;
   blacklist: string[];
 }
@@ -40,6 +41,10 @@ export class Settings {
     return this.json.interval;
   }
 
+  get debounce() {
+    return this.json.debounce;
+  }
+
   get startup() {
     return this.json.startup;
   }
diff --git a/server/src/utils/JavascriptUtils.ts b/server/src/utils/JavascriptUtils.ts
new file mode 100644
index 0000000..727ff06
--- /dev/null
+++ b/server/src/utils/JavascriptUtils.ts
@@ -0,0 +1,16 @@
+export const sleep = (ms: number, callback?: (cancelCallback: () => boolean) => void) =>
+  new Promise((resolve, reject) => {
+    let timeout: NodeJS.Timeout | undefined = setTimeout(() => {
+      timeout = undefined;
+      resolve();
+    }, ms);
+    callback?.(() => {
+      const timeoutSaved = timeout;
+      timeout = undefined;
+      reject();
+      if (timeoutSaved) {
+        clearTimeout(timeoutSaved);
+      }
+      return !!timeoutSaved;
+    });
+  });
diff --git a/server/src/utils/processes.ts b/server/src/utils/ProcessesUtils.ts
similarity index 100%
rename from server/src/utils/processes.ts
rename to server/src/utils/ProcessesUtils.ts
diff --git a/settings.json b/settings.json
index 741b814..ef5f625 100644
--- a/settings.json
+++ b/settings.json
@@ -1,6 +1,7 @@
 {
   "daemon": "C:\\\"Program Files\"\\LittleBigMouse\\LittleBigMouse_Daemon.exe",
   "interval": 1000,
+  "debounce": 10000,
   "startup": true,
   "blacklist": [
     "Back4Blood.exe",
@@ -24,4 +25,4 @@
     "war3.exe",
     "Warcraft III.exe"
   ]
-}
\ No newline at end of file
+}