summaryrefslogtreecommitdiff
path: root/frontend/src/tabs-hook.tsx
blob: 5929b8a0489fd529e9cc117ea305327e4d0b59d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { QuickAccessTab, quickAccessMenuClasses, sleep } from 'decky-frontend-lib';

import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
import Logger from './logger';

declare global {
  interface Window {
    __TABS_HOOK_INSTANCE: any;
  }
  interface Array<T> {
    __filter: any;
  }
}

const isTabsArray = (tabs: any) => {
  const length = tabs.length;
  return length >= 7 && tabs[length - 1]?.tab;
};

interface Tab {
  id: QuickAccessTab | number;
  title: any;
  content: any;
  icon: any;
}

class TabsHook extends Logger {
  // private keys = 7;
  tabs: Tab[] = [];
  private oFilter: (...args: any[]) => any;

  constructor() {
    super('TabsHook');

    this.log('Initialized');
    window.__TABS_HOOK_INSTANCE?.deinit?.();
    window.__TABS_HOOK_INSTANCE = this;

    const self = this;
    const oFilter = (this.oFilter = Array.prototype.filter);
    Array.prototype.filter = function patchedFilter(...args: any[]) {
      if (isTabsArray(this)) {
        self.render(this);
      }
      // @ts-ignore
      return oFilter.call(this, ...args);
    };

    if (document.title != 'SP')
      try {
        const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
        let qAMRoot: any;
        async function findQAMRoot(currentNode: any, iters: number): Promise<any> {
          if (iters >= 60) {
            // currently 44
            return null;
          }
          currentNode = currentNode?.child;
          if (
            currentNode?.memoizedProps?.className &&
            currentNode?.memoizedProps?.className.startsWith(quickAccessMenuClasses.ViewPlaceholder)
          ) {
            self.log(`QAM root was found in ${iters} recursion cycles`);
            return currentNode;
          }
          if (!currentNode) return null;
          if (currentNode.sibling) {
            let node = await findQAMRoot(currentNode.sibling, iters + 1);
            if (node !== null) return node;
          }
          return await findQAMRoot(currentNode, iters + 1);
        }
        (async () => {
          qAMRoot = await findQAMRoot(tree, 0);
          while (!qAMRoot) {
            this.error(
              'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
            );
            await sleep(5000);
            qAMRoot = await findQAMRoot(tree, 0);
          }

          while (!qAMRoot?.stateNode?.forceUpdate) {
            qAMRoot = qAMRoot.return;
          }
          qAMRoot.stateNode.shouldComponentUpdate = () => true;
          qAMRoot.stateNode.forceUpdate();
          delete qAMRoot.stateNode.shouldComponentUpdate;
        })();
      } catch (e) {
        this.log('Failed to rerender QAM', e);
      }
  }

  deinit() {
    Array.prototype.filter = this.oFilter;
  }

  add(tab: Tab) {
    this.debug('Adding tab', tab.id, 'to render array');
    this.tabs.push(tab);
  }

  removeById(id: number) {
    this.debug('Removing tab', id);
    this.tabs = this.tabs.filter((tab) => tab.id !== id);
  }

  render(existingTabs: any[]) {
    for (const { title, icon, content, id } of this.tabs) {
      existingTabs.push({
        key: id,
        title,
        tab: icon,
        decky: true,
        panel: <QuickAccessVisibleStateProvider>{content}</QuickAccessVisibleStateProvider>,
      });
    }
  }
}

export default TabsHook;