From 57f4555350c669e4cb098b48691975be79838468 Mon Sep 17 00:00:00 2001
From: Marco Rodolfi <marco.rodolfi@tuta.io>
Date: Mon, 19 Jun 2023 15:23:27 +0200
Subject: [Feature] File picker improvements (#454)

* First iteration for internationalization of the loader

* First iteration for internationalization of the loader

* Cleanup node mess

* Cleanup node mess pt2

* Additional touches

* Latest decky changed merged into i18n and updated translation.

* Styling fixes

* Initial backend hosting implementation

* Added correct url path of the loopback server.

* Added correct url path of the loopback server.

* Some better namespaced text.

* Added whitelist for locales path.

* Refactor languages and fix hooks logic bugs.

* Small typo in language translation structure.

* Working backend, automatically swtich languages with steam and language fixes.

* Fix to languages

* Key fixes

* Additional language fixes.

* Additional json changes

* Final text revision and added a vscode tasks to automatically extract text from code.

* Typo in the middleware

* Remove unused imports

* Cleanup whitespaces.

* Import changes

* Revert "Import changes"

This reverts commit 8e8231950fd7cc6cece87040e326d0a72ba79567.

* Update index.d.ts

* Clean up unused imports

* Delete pnpm-lock.yaml

* Update rollup.config.js

* Update PluginInstallModal.tsx

* Update index.tsx

* Update plugin-loader.tsx

* Update plugin-loader.tsx

* Revert "Delete pnpm-lock.yaml"

This reverts commit 3a39f36f2193cc976d36ffe07338239e363d5b04.

* Additional strings reworks.

* Fixes for issues coming from github merge.

* Fixes for master

* Styling fixes

* Styling pt2

* Missed a few strings in master,

* Styling fixes

* Additional master merge fixes.

* Final cleanup and adaptation to master.

* Final empty language cleanup and few string added

* Small changes to italian translation

* Disabled translation on a few components inside plugin-loader for missing react hooks.

* Fixed passing tag to translation.

* Disable debug output for reducing console spam.

* Return correct content type

* Small italian language change

* Added support for country code

* Fixed missing translation for uninstall popup.

* Fix class name shenanigans for  toast notification

* Update dependencies

* Fixed github workflow to include the new locales folder

* Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up

* Missed a file name change

* Updated dev dependencies to latest version

* Missed a few dev dependencies

* Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up"

Messed up merge with a different main branch

* Messed up deletion of rollup config.

* Fix broken pnpm lock file

* Missed a localized string during the merge

* Fixed a parameter mistake in the uninstall text parameter

* Fix pnpm random issues

* Small italian language tweaks

* Fix wrong parameter passed to the uninstall function call

* Another fix on a wrong function parameter

* Additional translation text on the store and branch selection channels

* Changed the default type passed to map to being able to index the two arrays.

* Reverted and reworked the last changes

* Distinguish events in UI for installing vs reinstalling plugins

* Additional fixes for reinstall prompt

* Revert the use of intevalPlural since the parser doesn't seem to support that.

* Missed a routing path in the backend

* Small bugfixes

* Small fixes

* Correctly adding the parameter to the request headers.

* Refactoring of the UI popup modal

* Fix pnpm shenanigans

* Final fixes for the install UI localization

* Clean up unnedeed backend code

* Small rework on text selection.

* Cleaned up parser configuration

* Removed extracttext dependency to pnpmsetup

* Merged translation and cleaned up parser

* Fixed JSON structure after manual merge.

* Added translation to the file picker

* First iteration for merging the new filepicker.

* Revert changes to PluginInstallModal

* Reworked the text modal for the final time

* Missed the proper linted text

* Missed the backend change

* Final branch cleanup

* First iteration for porting the new file picker

* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo

* Initial working upstream iteration for file picker

* Typo on translation variable

* File picker final improvements

* Stylistic fixes and fix on wrong bool passed to fp

* Fixup merge from main

* Other merge errors fixed

* Minor cleanups

* Fixed missing padding under text label extension

* Implement pagination backend side

* First draft for filtering backend side

* Implemented matching on file names.

* Fix for unable to order per size on folders.

* Hard checking a return value

* Added a missing import.

* Implemented show more as a frontend button

* Whoops, python typo

* Fixed python backend

* Rendering bug fix and small qol improvement

* Added missing parameter to openFilePicker call

* Fixed path on windows and unknown error on wrong path

* Small backend fixes

* Extension fix

* Simplified extension logic

* Less string conversions.

* Optimize backend code and removed additional components.

* Take correctly into account the max value

The button will now respect the actual maximum desired number of entries.

* Bugfix for ordering logic and ignore cases during sorting

* Regex call was missing an argument

* Fixed issues with filtering extensions

* Rollback testing changes

* Minor cleanup and attempt at fixing the not updating multimodal.

* Cleanup variable types.

* Mantains the same api format from the original source code.

* Removing hardcoded paths in the code

* Additional fixes for resolving the user path

* Cleanup useless modifications

* Final fixes for avoid path hardcoding

* Update lockfile and i18next version
---
 .../src/components/modals/DropdownMultiselect.tsx  | 121 +++++++++++++++++++++
 1 file changed, 121 insertions(+)
 create mode 100644 frontend/src/components/modals/DropdownMultiselect.tsx

(limited to 'frontend/src/components/modals/DropdownMultiselect.tsx')

diff --git a/frontend/src/components/modals/DropdownMultiselect.tsx b/frontend/src/components/modals/DropdownMultiselect.tsx
new file mode 100644
index 00000000..5defbfa4
--- /dev/null
+++ b/frontend/src/components/modals/DropdownMultiselect.tsx
@@ -0,0 +1,121 @@
+import {
+  DialogButton,
+  DialogCheckbox,
+  DialogCheckboxProps,
+  Marquee,
+  Menu,
+  MenuItem,
+  findModuleChild,
+  showContextMenu,
+} from 'decky-frontend-lib';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaChevronDown } from 'react-icons/fa';
+
+const dropDownControlButtonClass = findModuleChild((m) => {
+  if (typeof m !== 'object') return undefined;
+  for (const prop in m) {
+    if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) {
+      return m[prop];
+    }
+  }
+});
+
+const DropdownMultiselectItem: FC<
+  {
+    value: any;
+    onSelect: (checked: boolean, value: any) => void;
+    checked: boolean;
+  } & DialogCheckboxProps
+> = ({ value, onSelect, checked: defaultChecked, ...rest }) => {
+  const [checked, setChecked] = useState(defaultChecked);
+
+  useEffect(() => {
+    onSelect?.(checked, value);
+  }, [checked, onSelect, value]);
+
+  return (
+    <MenuItem bInteractableItem onClick={() => setChecked((x) => !x)}>
+      <DialogCheckbox
+        style={{ marginBottom: 0, padding: 0 }}
+        className="decky_DropdownMultiselectItem_DialogCheckbox"
+        bottomSeparator="none"
+        {...rest}
+        onClick={() => setChecked((x) => !x)}
+        onChange={(checked) => setChecked(checked)}
+        controlled
+        checked={checked}
+      />
+    </MenuItem>
+  );
+};
+
+const DropdownMultiselect: FC<{
+  items: {
+    label: string;
+    value: string;
+  }[];
+  selected: string[];
+  onSelect: (selected: any[]) => void;
+  label: string;
+}> = ({ label, items, selected, onSelect }) => {
+  const [itemsSelected, setItemsSelected] = useState<any>(selected);
+  const { t } = useTranslation();
+
+  const handleItemSelect = useCallback((checked, value) => {
+    setItemsSelected((x: any) =>
+      checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
+    );
+  }, []);
+
+  useEffect(() => {
+    onSelect(itemsSelected);
+  }, [itemsSelected, onSelect]);
+
+  return (
+    <DialogButton
+      style={{
+        display: 'flex',
+        alignItems: 'center',
+        maxWidth: '100%',
+      }}
+      className={dropDownControlButtonClass}
+      onClick={(evt) => {
+        evt.preventDefault();
+        showContextMenu(
+          <Menu label={label} cancelText={t('DropdownMultiselect.button.back') as string}>
+            <style>
+              {`
+              /* Inherit color from ".basiccontextmenu" */
+              .decky_DropdownMultiselectItem_DialogCheckbox > .DialogToggle_Label {
+                color: inherit;
+              }
+              `}
+            </style>
+            <div style={{ marginTop: '10px' }}>{/*FIXME: Hack for missing padding under label menu*/}</div>
+            {items.map((x) => (
+              <DropdownMultiselectItem
+                key={x.value}
+                label={x.label}
+                value={x.value}
+                checked={itemsSelected.includes(x.value)}
+                onSelect={handleItemSelect}
+              />
+            ))}
+          </Menu>,
+          evt.currentTarget ?? window,
+        );
+      }}
+    >
+      <Marquee>
+        {selected.length > 0
+          ? selected.map((x: any) => items[items.findIndex((v) => v.value === x)].label).join(', ')
+          : '…'}
+      </Marquee>
+      <div style={{ flexGrow: 1, minWidth: '1ch' }} />
+      <FaChevronDown style={{ height: '1em', flex: '0 0 1em' }} />
+    </DialogButton>
+  );
+};
+
+export default DropdownMultiselect;
-- 
cgit v1.2.3

