Skip to content

Commit

Permalink
feat: make dialog controllable with gamepad
Browse files Browse the repository at this point in the history
  • Loading branch information
bmsuseluda committed Mar 21, 2024
1 parent 406d37e commit c79f1db
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 125 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useLoaderData, useSubmit } from "@remix-run/react";
import { ErrorDialog } from "~/components/ErrorDialog";
import { useFocus } from "~/hooks/useFocus";
import type { FocusElement } from "~/types/focusElement";
import { useCallback, useEffect } from "react";
import type { ElementRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import {
useGamepadButtonPressEvent,
useKeyboardEvent,
Expand Down Expand Up @@ -34,6 +35,7 @@ export const ErrorBoundary = ({ error }: { error: Error }) => {

export default function RenderComponent() {
const { errorDialog } = useLoaderData<typeof loader>();
const listRef = useRef<ElementRef<"div">>(null);
const submit = useSubmit();
const { switchFocusBack, switchFocus, isInFocus } =
useFocus<FocusElement>("errorDialog");
Expand All @@ -55,13 +57,39 @@ export default function RenderComponent() {
}
}, [switchFocusBack, submit, isInFocus]);

const handleScrollDown = useCallback(() => {
if (isInFocus && listRef?.current) {
const scrollHeight = listRef.current.clientHeight;
const scrollTop = listRef.current.scrollTop;
listRef.current.scroll({
behavior: "smooth",
top: scrollTop + scrollHeight / 2,
});
}
}, [isInFocus]);

const handleScrollUp = useCallback(() => {
if (isInFocus && listRef?.current) {
const scrollHeight = listRef.current.clientHeight;
const scrollTop = listRef.current.scrollTop;
listRef.current.scroll({
behavior: "smooth",
top: scrollTop - scrollHeight / 2,
});
}
}, [isInFocus]);

useGamepadButtonPressEvent(layout.buttons.X, handleClose);
useGamepadButtonPressEvent(layout.buttons.B, handleClose);
useGamepadButtonPressEvent(layout.buttons.Start, handleClose);

useKeyboardEvent("Enter", handleClose);
useKeyboardEvent("Escape", handleClose);
useKeyboardEvent("Backspace", handleClose);
useKeyboardEvent("ArrowDown", handleScrollDown);
useKeyboardEvent("ArrowUp", handleScrollUp);

return <ErrorDialog {...errorDialog} onClose={handleClose} />;
return (
<ErrorDialog {...errorDialog} onClose={handleClose} listRef={listRef} />

Check failure on line 93 in app/customRoutes/categories.$category.errorDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

Type '{ onClose: () => void; listRef: RefObject<HTMLDivElement>; title?: string | undefined; stacktrace?: string | undefined; }' is not assignable to type 'IntrinsicAttributes & Props'.
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,81 +121,85 @@ type ActionReturn = {
};

export const action = async ({ request }: ActionFunctionArgs) => {
const form = await request.formData();
const _actionId = form.get("_actionId");
const applicationsPath = form.get("applicationsPath")?.toString();
const categoriesPath = form.get("categoriesPath")?.toString();

if (_actionId === actionIds.importAll) {
const errors: Errors = {};

if (isWindows()) {
const errorApplicationsPath = validatePath(
applicationsPathLabel,
applicationsPath,
try {
const form = await request.formData();
const _actionId = form.get("_actionId");
const applicationsPath = form.get("applicationsPath")?.toString();
const categoriesPath = form.get("categoriesPath")?.toString();

if (_actionId === actionIds.importAll) {
const errors: Errors = {};

if (isWindows()) {
const errorApplicationsPath = validatePath(
applicationsPathLabel,
applicationsPath,
);
if (errorApplicationsPath) {
errors.applicationsPath = errorApplicationsPath;
}
}
const errorCategoriesPath = validatePath(
categoriesPathLabel,
categoriesPath,
);
if (errorApplicationsPath) {
errors.applicationsPath = errorApplicationsPath;
if (errorCategoriesPath) {
errors.categoriesPath = errorCategoriesPath;
}
}
const errorCategoriesPath = validatePath(
categoriesPathLabel,
categoriesPath,
);
if (errorCategoriesPath) {
errors.categoriesPath = errorCategoriesPath;
}

if (Object.keys(errors).length > 0) {
return json({ errors });
}
if (Object.keys(errors).length > 0) {
return json({ errors });
}

const fields: General = {
applicationsPath:
isWindows() && typeof applicationsPath === "string"
? applicationsPath
: undefined,
categoriesPath,
};
const fields: General = {
applicationsPath:
isWindows() && typeof applicationsPath === "string"
? applicationsPath
: undefined,
categoriesPath,
};

writeGeneral(fields);
await importCategories();
writeGeneral(fields);
await importCategories();

const categories = readCategories();
const categories = readCategories();

if (categories?.length > 0) {
throw redirect(`/categories/${categories[0].id}/settings/general`);
if (categories?.length > 0) {
throw redirect(`/categories/${categories[0].id}/settings/general`);
}
}
}

if (_actionId === actionIds.installMissingApplications) {
await installMissingApplicationsOnLinux();
}
if (_actionId === actionIds.installMissingApplications) {
await installMissingApplicationsOnLinux();
}

if (_actionId === actionIds.chooseApplicationsPath) {
const newApplicationsPath = openFolderDialog(
"Select Emulators Folder",
typeof applicationsPath === "string" ? applicationsPath : undefined,
);
if (newApplicationsPath) {
return json({
applicationsPath: newApplicationsPath,
categoriesPath,
});
if (_actionId === actionIds.chooseApplicationsPath) {
const newApplicationsPath = openFolderDialog(
"Select Emulators Folder",
typeof applicationsPath === "string" ? applicationsPath : undefined,
);
if (newApplicationsPath) {
return json({
applicationsPath: newApplicationsPath,
categoriesPath,
});
}
}
}

if (_actionId === actionIds.chooseCategoriesPath) {
const newCategoriesPath = openFolderDialog(
"Select Roms Folder",
categoriesPath,
);
if (newCategoriesPath) {
return json({
applicationsPath,
categoriesPath: newCategoriesPath,
});
if (_actionId === actionIds.chooseCategoriesPath) {
const newCategoriesPath = openFolderDialog(
"Select Roms Folder",
categoriesPath,
);
if (newCategoriesPath) {
return json({
applicationsPath,
categoriesPath: newCategoriesPath,
});
}
}
} catch (e) {
return redirect("errorDialog");
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const loader = ({ params }: DataFunctionArgs) => {
const categoryData = readCategory(category as SystemId);

if (!categoryData?.name) {
throw redirect("settings");
return redirect("settings");
}

const { alwaysGameNames } = readAppearance();
Expand All @@ -64,40 +64,40 @@ const actionIds = {
};

export const action: ActionFunction = async ({ request, params }) => {
const { category } = params;
if (!category) {
console.log("category empty");
throw Error("category empty");
}
try {
const { category } = params;
if (!category) {
console.log("category empty");
throw Error("category empty");
}

const general = readGeneral();
const categoryData = readCategory(category as SystemId);
const general = readGeneral();
const categoryData = readCategory(category as SystemId);

if (
!general?.categoriesPath ||
!categoryData?.name ||
!fs.existsSync(nodepath.join(general.categoriesPath, categoryData.name))
) {
throw redirect("settings");
}
if (
!general?.categoriesPath ||
!categoryData?.name ||
!fs.existsSync(nodepath.join(general.categoriesPath, categoryData.name))
) {
return redirect("settings");
}

const form = await request.formData();
const _actionId = form.get("_actionId");
const form = await request.formData();
const _actionId = form.get("_actionId");

if (_actionId === actionIds.launch) {
const game = form.get("game");
if (typeof game === "string") {
try {
if (_actionId === actionIds.launch) {
const game = form.get("game");
if (typeof game === "string") {
executeApplication(category as SystemId, game);
return { ok: true };
} catch (e) {
throw redirect("errorDialog");
}
}
}

if (_actionId === actionIds.import) {
await importEntries(category as SystemId);
if (_actionId === actionIds.import) {
await importEntries(category as SystemId);
}
} catch (e) {
return redirect("errorDialog");
}

return null;
Expand Down Expand Up @@ -188,14 +188,15 @@ export default function Category() {
switchFocus("main");
enableGamepads();
}
}, [isInFocus, enableGamepads, switchFocus]);

const onSelectEntryByGamepad = useCallback(() => {
if (listRef?.current) {
// Add scrollPadding if entry was selected by gamepad to center the element.
listRef.current.style.scrollPadding = scrollPadding;
}
}, []);
setTimeout(() => {
if (listRef?.current) {
// Add scrollPadding if entry was selected by gamepad to center the element.
// Needs to be in a timeout to reactivate the feature afterwards
listRef.current.style.scrollPadding = scrollPadding;
}
}, 10);
}, [isInFocus, enableGamepads, switchFocus]);

if (!categoryData) {
return null;
Expand Down Expand Up @@ -228,7 +229,6 @@ export default function Category() {
onBack={onBack}
isInFocus={isInFocus}
onGameClick={onEntryClick}
onSelectGameByGamepad={onSelectEntryByGamepad}
{...getTestId("entries")}
/>
)
Expand Down
File renamed without changes.
46 changes: 46 additions & 0 deletions buildConfig/remix.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var createErrorDialog = function (route, id) {
route("errorDialog", "customRoutes/categories.$category.errorDialog.tsx", {
id: "".concat(id, "ErrorDialog"),
});
};
var createSettingsRoutes = function (route, id) {
route("settings", "customRoutes/categories.$category.settings.tsx", {
id: id,
}, function () {
route("", "customRoutes/categories.$category.settings._index.tsx", {
index: true,
id: "".concat(id, "SettingsIndex"),
});
route("general", "customRoutes/categories.$category.settings.general.tsx", {
id: "".concat(id, "SettingsGeneral"),
}, function () {
createErrorDialog(route, "".concat(id, "SettingsGeneral"));
});
route("appearance", "customRoutes/categories.$category.settings.appearance.tsx", {
id: "".concat(id, "SettingsAppearance"),
});
});
};
var createCategoriesRoutes = function (route) {
route("categories", "customRoutes/categories.tsx", {}, function () {
route(":category", "customRoutes/categories.$category.tsx", function () {
createSettingsRoutes(route, "category");
createErrorDialog(route, "category");
});
});
};
var appConfig = {
appDirectory: "app",
serverModuleFormat: "cjs",
routes: function (defineRoutes) {
return defineRoutes(function (route) {
route("/", "customRoutes/_index.tsx", { index: true });
createCategoriesRoutes(route);
createSettingsRoutes(route, "initial");
});
},
postcss: true,
};
exports.default = appConfig;
Loading

0 comments on commit c79f1db

Please sign in to comment.