diff --git a/src/main/entity/game-shop-cache.entity.ts b/src/main/entity/game-shop-cache.entity.ts index f83e1b0c..3382da1c 100644 --- a/src/main/entity/game-shop-cache.entity.ts +++ b/src/main/entity/game-shop-cache.entity.ts @@ -18,6 +18,9 @@ export class GameShopCache { @Column("text", { nullable: true }) serializedData: string; + /** + * @deprecated Use IndexedDB's `howLongToBeatEntries` instead + */ @Column("text", { nullable: true }) howLongToBeatSerializedData: string; diff --git a/src/main/events/catalogue/get-how-long-to-beat.ts b/src/main/events/catalogue/get-how-long-to-beat.ts index f489f804..01966afc 100644 --- a/src/main/events/catalogue/get-how-long-to-beat.ts +++ b/src/main/events/catalogue/get-how-long-to-beat.ts @@ -1,45 +1,23 @@ -import type { GameShop, HowLongToBeatCategory } from "@types"; +import type { HowLongToBeatCategory } from "@types"; import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services"; import { registerEvent } from "../register-event"; -import { gameShopCacheRepository } from "@main/repository"; +import { formatName } from "@shared"; const getHowLongToBeat = async ( _event: Electron.IpcMainInvokeEvent, - objectId: string, - shop: GameShop, title: string ): Promise => { - const searchHowLongToBeatPromise = searchHowLongToBeat(title); + const response = await searchHowLongToBeat(title); - const gameShopCache = await gameShopCacheRepository.findOne({ - where: { objectID: objectId, shop }, + const game = response.data.find((game) => { + return formatName(game.game_name) === formatName(title); }); - const howLongToBeatCachedData = gameShopCache?.howLongToBeatSerializedData - ? JSON.parse(gameShopCache?.howLongToBeatSerializedData) - : null; - if (howLongToBeatCachedData) return howLongToBeatCachedData; + if (!game) return null; + const howLongToBeat = await getHowLongToBeatGame(String(game.game_id)); - return searchHowLongToBeatPromise.then(async (response) => { - const game = response.data.find( - (game) => game.profile_steam === Number(objectId) - ); - - if (!game) return null; - const howLongToBeat = await getHowLongToBeatGame(String(game.game_id)); - - gameShopCacheRepository.upsert( - { - objectID: objectId, - shop, - howLongToBeatSerializedData: JSON.stringify(howLongToBeat), - }, - ["objectID"] - ); - - return howLongToBeat; - }); + return howLongToBeat; }; registerEvent("getHowLongToBeat", getHowLongToBeat); diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index eb80bc47..b1244684 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -48,7 +48,9 @@ const updateProfile = async ( const profileImageUrl = await getNewProfileImageUrl( updateProfile.profileImageUrl - ).catch(() => undefined); + ).catch((err) => { + console.log(err); + }); return patchUserProfile({ ...updateProfile, profileImageUrl }); }; diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts index c7164d09..5a82d8e7 100644 --- a/src/main/services/how-long-to-beat.ts +++ b/src/main/services/how-long-to-beat.ts @@ -1,32 +1,65 @@ import axios from "axios"; import { requestWebPage } from "@main/helpers"; -import { HowLongToBeatCategory } from "@types"; +import type { + HowLongToBeatCategory, + HowLongToBeatSearchResponse, +} from "@types"; import { formatName } from "@shared"; import { logger } from "./logger"; +import UserAgent from "user-agents"; -export interface HowLongToBeatResult { - game_id: number; - profile_steam: number; -} +const state = { + apiKey: null as string | null, +}; -export interface HowLongToBeatSearchResponse { - data: HowLongToBeatResult[]; -} +const getHowLongToBeatSearchApiKey = async () => { + const userAgent = new UserAgent(); + + const document = await requestWebPage("https://howlongtobeat.com/"); + const scripts = Array.from(document.querySelectorAll("script")); + + const appScript = scripts.find((script) => + script.src.startsWith("/_next/static/chunks/pages/_app") + ); + + if (!appScript) return null; + + const response = await axios.get( + `https://howlongtobeat.com${appScript.src}`, + { + headers: { + "User-Agent": userAgent.toString(), + }, + } + ); + + const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec( + response.data + ); + + if (!results) return null; + + return results[1]; +}; export const searchHowLongToBeat = async (gameName: string) => { + state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey()); + if (!state.apiKey) return { data: [] }; + + const userAgent = new UserAgent(); + const response = await axios .post( - "https://howlongtobeat.com/api/search", + "https://howlongtobeat.com/api/search/8fbd64723a8204dd", { searchType: "games", searchTerms: formatName(gameName).split(" "), searchPage: 1, - size: 100, + size: 20, }, { headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "User-Agent": userAgent.toString(), Referer: "https://howlongtobeat.com/", }, } diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 9b9d09b2..164ccf9c 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -16,6 +16,7 @@ import trayIcon from "@resources/tray-icon.png?asset"; import { gameRepository, userPreferencesRepository } from "@main/repository"; import { IsNull, Not } from "typeorm"; import { HydraApi } from "./hydra-api"; +import UserAgent from "user-agents"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; @@ -79,11 +80,12 @@ export class WindowManager { this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( (details, callback) => { + const userAgent = new UserAgent(); + callback({ requestHeaders: { ...details.requestHeaders, - "user-agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "user-agent": userAgent.toString(), }, }); } @@ -146,30 +148,29 @@ export class WindowManager { } public static createNotificationWindow() { - this.notificationWindow = new BrowserWindow({ - transparent: true, - maximizable: false, - autoHideMenuBar: true, - minimizable: false, - focusable: false, - skipTaskbar: true, - frame: false, - width: 350, - height: 104, - x: 0, - y: 0, - webPreferences: { - preload: path.join(__dirname, "../preload/index.mjs"), - sandbox: false, - }, - }); - - this.notificationWindow.setIgnoreMouseEvents(true); - this.notificationWindow.setVisibleOnAllWorkspaces(true, { - visibleOnFullScreen: true, - }); - this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1); - this.loadNotificationWindowURL(); + // this.notificationWindow = new BrowserWindow({ + // transparent: true, + // maximizable: false, + // autoHideMenuBar: true, + // minimizable: false, + // focusable: false, + // skipTaskbar: true, + // frame: false, + // width: 350, + // height: 104, + // x: 0, + // y: 0, + // webPreferences: { + // preload: path.join(__dirname, "../preload/index.mjs"), + // sandbox: false, + // }, + // }); + // this.notificationWindow.setIgnoreMouseEvents(true); + // this.notificationWindow.setVisibleOnAllWorkspaces(true, { + // visibleOnFullScreen: true, + // }); + // this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1); + // this.loadNotificationWindowURL(); } public static openAuthWindow() { diff --git a/src/preload/index.ts b/src/preload/index.ts index 761696f9..decd407c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -41,8 +41,8 @@ contextBridge.exposeInMainWorld("electron", { getGameShopDetails: (objectId: string, shop: GameShop, language: string) => ipcRenderer.invoke("getGameShopDetails", objectId, shop, language), getRandomGame: () => ipcRenderer.invoke("getRandomGame"), - getHowLongToBeat: (objectId: string, shop: GameShop, title: string) => - ipcRenderer.invoke("getHowLongToBeat", objectId, shop, title), + getHowLongToBeat: (title: string) => + ipcRenderer.invoke("getHowLongToBeat", title), getGames: (take?: number, skip?: number) => ipcRenderer.invoke("getGames", take, skip), searchGameRepacks: (query: string) => diff --git a/src/renderer/src/context/repacks/repacks.context.tsx b/src/renderer/src/context/repacks/repacks.context.tsx index 054df60b..b688793c 100644 --- a/src/renderer/src/context/repacks/repacks.context.tsx +++ b/src/renderer/src/context/repacks/repacks.context.tsx @@ -41,10 +41,12 @@ export function RepacksContextProvider({ children }: RepacksContextProps) { }, []); const indexRepacks = useCallback(() => { + console.log("INDEXING"); setIsIndexingRepacks(true); repacksWorker.postMessage("INDEX_REPACKS"); repacksWorker.onmessage = () => { + console.log("INDEXING COMPLETE"); setIsIndexingRepacks(false); }; }, []); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index fe4d3112..2896abf3 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -58,8 +58,6 @@ declare global { ) => Promise; getRandomGame: () => Promise; getHowLongToBeat: ( - objectId: string, - shop: GameShop, title: string ) => Promise; getGames: (take?: number, skip?: number) => Promise; diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts index 75dc6079..e0e86a7f 100644 --- a/src/renderer/src/dexie.ts +++ b/src/renderer/src/dexie.ts @@ -1,4 +1,4 @@ -import { GameShop } from "@types"; +import type { GameShop, HowLongToBeatCategory } from "@types"; import { Dexie } from "dexie"; export interface GameBackup { @@ -8,16 +8,29 @@ export interface GameBackup { createdAt: Date; } +export interface HowLongToBeatEntry { + id?: number; + objectId: string; + categories: HowLongToBeatCategory[]; + shop: GameShop; + createdAt: Date; + updatedAt: Date; +} + export const db = new Dexie("Hydra"); -db.version(3).stores({ +db.version(4).stores({ repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, gameBackups: `++id, [shop+objectId], createdAt`, + howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`, }); export const downloadSourcesTable = db.table("downloadSources"); export const repacksTable = db.table("repacks"); export const gameBackupsTable = db.table("gameBackups"); +export const howLongToBeatEntriesTable = db.table( + "howLongToBeatEntries" +); db.open(); diff --git a/src/renderer/src/pages/game-details/game-details-skeleton.tsx b/src/renderer/src/pages/game-details/game-details-skeleton.tsx index 23f0c6f1..24cfe2cb 100644 --- a/src/renderer/src/pages/game-details/game-details-skeleton.tsx +++ b/src/renderer/src/pages/game-details/game-details-skeleton.tsx @@ -43,23 +43,6 @@ export function GameDetailsSkeleton() {
- {/*
-

HowLongToBeat

-
-
    - {Array.from({ length: 3 }).map((_, index) => ( - - ))} -
*/} -
-

{t("requirements")}

-
+ +
+ {children} +
+
+ ); +} diff --git a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx index ffd148e5..d63879f5 100644 --- a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx @@ -4,6 +4,7 @@ import type { HowLongToBeatCategory } from "@types"; import { vars } from "@renderer/theme.css"; import * as styles from "./sidebar.css"; +import { SidebarSection } from "../sidebar-section/sidebar-section"; const durationTranslation: Record = { Hours: "hours", @@ -30,41 +31,42 @@ export function HowLongToBeatSection({ return ( -
-

HowLongToBeat

-
- -
    - {howLongToBeatData - ? howLongToBeatData.map((category) => ( -
  • -

    +

      + {howLongToBeatData + ? howLongToBeatData.map((category) => ( +
    • - {category.title} -

      +

      + {category.title} +

      -

      - {getDuration(category.duration)} -

      +

      + {getDuration(category.duration)} +

      - {category.accuracy !== "00" && ( - - {t("accuracy", { accuracy: category.accuracy })} - - )} -
    • - )) - : Array.from({ length: 4 }).map((_, index) => ( - - ))} -
    + {category.accuracy !== "00" && ( + + {t("accuracy", { accuracy: category.accuracy })} + + )} +
  • + )) + : Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+
); } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts index 783e4ffa..c909fba2 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts @@ -3,7 +3,8 @@ import { globalStyle, style } from "@vanilla-extract/css"; import { SPACING_UNIT, vars } from "../../../theme.css"; export const contentSidebar = style({ - borderLeft: `solid 1px ${vars.color.border};`, + borderLeft: `solid 1px ${vars.color.border}`, + backgroundColor: vars.color.darkBackground, width: "100%", height: "100%", "@media": { @@ -18,14 +19,6 @@ export const contentSidebar = style({ }, }); -export const contentSidebarTitle = style({ - height: "72px", - padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, - display: "flex", - alignItems: "center", - backgroundColor: vars.color.background, -}); - export const requirementButtonContainer = style({ width: "100%", display: "flex", @@ -55,7 +48,7 @@ export const requirementsDetailsSkeleton = style({ export const howLongToBeatCategoriesList = style({ margin: "0", - padding: "16px", + padding: `${SPACING_UNIT * 2}px`, display: "flex", flexDirection: "column", gap: "16px", @@ -65,7 +58,8 @@ export const howLongToBeatCategory = style({ display: "flex", flexDirection: "column", gap: "4px", - backgroundColor: vars.color.background, + background: + "linear-gradient(90deg, transparent 20%, rgb(255 255 255 / 2%) 100%)", borderRadius: "4px", padding: `8px 16px`, border: `solid 1px ${vars.color.border}`, @@ -86,6 +80,8 @@ export const statsSection = style({ gap: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`, justifyContent: "space-between", + transition: "max-height ease 0.5s", + overflow: "hidden", "@media": { "(min-width: 1024px)": { flexDirection: "column", diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 7b748a1a..5c3cbbc0 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import type { HowLongToBeatCategory, SteamAppDetails } from "@types"; import { useTranslation } from "react-i18next"; import { Button } from "@renderer/components"; @@ -8,9 +8,12 @@ import { gameDetailsContext } from "@renderer/context"; import { useDate, useFormat } from "@renderer/hooks"; import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { HowLongToBeatSection } from "./how-long-to-beat-section"; +import { howLongToBeatEntriesTable } from "@renderer/dexie"; +import { SidebarSection } from "../sidebar-section/sidebar-section"; export function Sidebar() { - const [_howLongToBeat, _setHowLongToBeat] = useState<{ + const [howLongToBeat, setHowLongToBeat] = useState<{ isLoading: boolean; data: HowLongToBeatCategory[] | null; }>({ isLoading: true, data: null }); @@ -18,7 +21,7 @@ export function Sidebar() { const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { gameTitle, shopDetails, stats, achievements } = + const { gameTitle, shopDetails, objectId, shop, stats, achievements } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); @@ -26,28 +29,45 @@ export function Sidebar() { const { numberFormatter } = useFormat(); - // useEffect(() => { - // if (objectId) { - // setHowLongToBeat({ isLoading: true, data: null }); + useEffect(() => { + if (objectId) { + setHowLongToBeat({ isLoading: true, data: null }); - // window.electron - // .getHowLongToBeat(objectId, "steam", gameTitle) - // .then((howLongToBeat) => { - // setHowLongToBeat({ isLoading: false, data: howLongToBeat }); - // }) - // .catch(() => { - // setHowLongToBeat({ isLoading: false, data: null }); - // }); - // } - // }, [objectId, gameTitle]); + howLongToBeatEntriesTable + .where({ shop, objectId }) + .first() + .then(async (cachedHowLongToBeat) => { + if (cachedHowLongToBeat) { + setHowLongToBeat({ + isLoading: false, + data: cachedHowLongToBeat.categories, + }); + } else { + try { + const howLongToBeat = + await window.electron.getHowLongToBeat(gameTitle); + + if (howLongToBeat) { + howLongToBeatEntriesTable.add({ + objectId, + shop: "steam", + createdAt: new Date(), + updatedAt: new Date(), + categories: howLongToBeat, + }); + } + + setHowLongToBeat({ isLoading: false, data: howLongToBeat }); + } catch (err) { + setHowLongToBeat({ isLoading: false, data: null }); + } + } + }); + } + }, [objectId, shop, gameTitle]); return ( ); } diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx index cd43641a..0d86bddc 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx @@ -64,6 +64,8 @@ export function EditProfileModal( const { showSuccessToast, showErrorToast } = useToast(); const onSubmit = async (values: FormValues) => { + console.log(values); + return patchUser(values) .then(async () => { await Promise.allSettled([fetchUserDetails(), getUserProfile()]); @@ -118,6 +120,8 @@ export function EditProfileModal( return { imagePath: null }; }); + console.log(imagePath); + onChange(imagePath); } }; diff --git a/src/renderer/src/workers/repacks.worker.ts b/src/renderer/src/workers/repacks.worker.ts index c4660074..6c3aca73 100644 --- a/src/renderer/src/workers/repacks.worker.ts +++ b/src/renderer/src/workers/repacks.worker.ts @@ -3,20 +3,21 @@ import { formatName } from "@shared"; import { GameRepack } from "@types"; import flexSearch from "flexsearch"; -const index = new flexSearch.Index(); - interface SerializedGameRepack extends Omit { uris: string; } const state = { repacks: [] as SerializedGameRepack[], + index: null as flexSearch.Index | null, }; self.onmessage = async ( event: MessageEvent<[string, string] | "INDEX_REPACKS"> ) => { if (event.data === "INDEX_REPACKS") { + state.index = new flexSearch.Index(); + repacksTable .toCollection() .sortBy("uploadDate") @@ -26,7 +27,7 @@ self.onmessage = async ( for (let i = 0; i < state.repacks.length; i++) { const repack = state.repacks[i]; const formattedTitle = formatName(repack.title); - index.add(i, formattedTitle); + state.index!.add(i, formattedTitle); } self.postMessage("INDEXING_COMPLETE"); @@ -34,7 +35,7 @@ self.onmessage = async ( } else { const [requestId, query] = event.data; - const results = index.search(formatName(query)).map((index) => { + const results = state.index!.search(formatName(query)).map((index) => { const repack = state.repacks.at(index as number) as SerializedGameRepack; return { diff --git a/src/shared/char-map.ts b/src/shared/char-map.ts new file mode 100644 index 00000000..7f29509e --- /dev/null +++ b/src/shared/char-map.ts @@ -0,0 +1,461 @@ +export const charMap = { + À: "A", + Á: "A", + Â: "A", + Ã: "A", + Ä: "A", + Å: "A", + Ấ: "A", + Ắ: "A", + Ẳ: "A", + Ẵ: "A", + Ặ: "A", + Æ: "AE", + Ầ: "A", + Ằ: "A", + Ȃ: "A", + Ả: "A", + Ạ: "A", + Ẩ: "A", + Ẫ: "A", + Ậ: "A", + Ç: "C", + Ḉ: "C", + È: "E", + É: "E", + Ê: "E", + Ë: "E", + Ế: "E", + Ḗ: "E", + Ề: "E", + Ḕ: "E", + Ḝ: "E", + Ȇ: "E", + Ẻ: "E", + Ẽ: "E", + Ẹ: "E", + Ể: "E", + Ễ: "E", + Ệ: "E", + Ì: "I", + Í: "I", + Î: "I", + Ï: "I", + Ḯ: "I", + Ȋ: "I", + Ỉ: "I", + Ị: "I", + Ð: "D", + Ñ: "N", + Ò: "O", + Ó: "O", + Ô: "O", + Õ: "O", + Ö: "O", + Ø: "O", + Ố: "O", + Ṍ: "O", + Ṓ: "O", + Ȏ: "O", + Ỏ: "O", + Ọ: "O", + Ổ: "O", + Ỗ: "O", + Ộ: "O", + Ờ: "O", + Ở: "O", + Ỡ: "O", + Ớ: "O", + Ợ: "O", + Ù: "U", + Ú: "U", + Û: "U", + Ü: "U", + Ủ: "U", + Ụ: "U", + Ử: "U", + Ữ: "U", + Ự: "U", + Ý: "Y", + à: "a", + á: "a", + â: "a", + ã: "a", + ä: "a", + å: "a", + ấ: "a", + ắ: "a", + ẳ: "a", + ẵ: "a", + ặ: "a", + æ: "ae", + ầ: "a", + ằ: "a", + ȃ: "a", + ả: "a", + ạ: "a", + ẩ: "a", + ẫ: "a", + ậ: "a", + ç: "c", + ḉ: "c", + è: "e", + é: "e", + ê: "e", + ë: "e", + ế: "e", + ḗ: "e", + ề: "e", + ḕ: "e", + ḝ: "e", + ȇ: "e", + ẻ: "e", + ẽ: "e", + ẹ: "e", + ể: "e", + ễ: "e", + ệ: "e", + ì: "i", + í: "i", + î: "i", + ï: "i", + ḯ: "i", + ȋ: "i", + ỉ: "i", + ị: "i", + ð: "d", + ñ: "n", + ò: "o", + ó: "o", + ô: "o", + õ: "o", + ö: "o", + ø: "o", + ố: "o", + ṍ: "o", + ṓ: "o", + ȏ: "o", + ỏ: "o", + ọ: "o", + ổ: "o", + ỗ: "o", + ộ: "o", + ờ: "o", + ở: "o", + ỡ: "o", + ớ: "o", + ợ: "o", + ù: "u", + ú: "u", + û: "u", + ü: "u", + ủ: "u", + ụ: "u", + ử: "u", + ữ: "u", + ự: "u", + ý: "y", + ÿ: "y", + Ā: "A", + ā: "a", + Ă: "A", + ă: "a", + Ą: "A", + ą: "a", + Ć: "C", + ć: "c", + Ĉ: "C", + ĉ: "c", + Ċ: "C", + ċ: "c", + Č: "C", + č: "c", + C̆: "C", + c̆: "c", + Ď: "D", + ď: "d", + Đ: "D", + đ: "d", + Ē: "E", + ē: "e", + Ĕ: "E", + ĕ: "e", + Ė: "E", + ė: "e", + Ę: "E", + ę: "e", + Ě: "E", + ě: "e", + Ĝ: "G", + Ǵ: "G", + ĝ: "g", + ǵ: "g", + Ğ: "G", + ğ: "g", + Ġ: "G", + ġ: "g", + Ģ: "G", + ģ: "g", + Ĥ: "H", + ĥ: "h", + Ħ: "H", + ħ: "h", + Ḫ: "H", + ḫ: "h", + Ĩ: "I", + ĩ: "i", + Ī: "I", + ī: "i", + Ĭ: "I", + ĭ: "i", + Į: "I", + į: "i", + İ: "I", + ı: "i", + IJ: "IJ", + ij: "ij", + Ĵ: "J", + ĵ: "j", + Ķ: "K", + ķ: "k", + Ḱ: "K", + ḱ: "k", + K̆: "K", + k̆: "k", + Ĺ: "L", + ĺ: "l", + Ļ: "L", + ļ: "l", + Ľ: "L", + ľ: "l", + Ŀ: "L", + ŀ: "l", + Ł: "l", + ł: "l", + Ḿ: "M", + ḿ: "m", + M̆: "M", + m̆: "m", + Ń: "N", + ń: "n", + Ņ: "N", + ņ: "n", + Ň: "N", + ň: "n", + ʼn: "n", + N̆: "N", + n̆: "n", + Ō: "O", + ō: "o", + Ŏ: "O", + ŏ: "o", + Ő: "O", + ő: "o", + Œ: "OE", + œ: "oe", + P̆: "P", + p̆: "p", + Ŕ: "R", + ŕ: "r", + Ŗ: "R", + ŗ: "r", + Ř: "R", + ř: "r", + R̆: "R", + r̆: "r", + Ȓ: "R", + ȓ: "r", + Ś: "S", + ś: "s", + Ŝ: "S", + ŝ: "s", + Ş: "S", + Ș: "S", + ș: "s", + ş: "s", + Š: "S", + š: "s", + Ţ: "T", + ţ: "t", + ț: "t", + Ț: "T", + Ť: "T", + ť: "t", + Ŧ: "T", + ŧ: "t", + T̆: "T", + t̆: "t", + Ũ: "U", + ũ: "u", + Ū: "U", + ū: "u", + Ŭ: "U", + ŭ: "u", + Ů: "U", + ů: "u", + Ű: "U", + ű: "u", + Ų: "U", + ų: "u", + Ȗ: "U", + ȗ: "u", + V̆: "V", + v̆: "v", + Ŵ: "W", + ŵ: "w", + Ẃ: "W", + ẃ: "w", + X̆: "X", + x̆: "x", + Ŷ: "Y", + ŷ: "y", + Ÿ: "Y", + Y̆: "Y", + y̆: "y", + Ź: "Z", + ź: "z", + Ż: "Z", + ż: "z", + Ž: "Z", + ž: "z", + ſ: "s", + ƒ: "f", + Ơ: "O", + ơ: "o", + Ư: "U", + ư: "u", + Ǎ: "A", + ǎ: "a", + Ǐ: "I", + ǐ: "i", + Ǒ: "O", + ǒ: "o", + Ǔ: "U", + ǔ: "u", + Ǖ: "U", + ǖ: "u", + Ǘ: "U", + ǘ: "u", + Ǚ: "U", + ǚ: "u", + Ǜ: "U", + ǜ: "u", + Ứ: "U", + ứ: "u", + Ṹ: "U", + ṹ: "u", + Ǻ: "A", + ǻ: "a", + Ǽ: "AE", + ǽ: "ae", + Ǿ: "O", + ǿ: "o", + Þ: "TH", + þ: "th", + Ṕ: "P", + ṕ: "p", + Ṥ: "S", + ṥ: "s", + X́: "X", + x́: "x", + Ѓ: "Г", + ѓ: "г", + Ќ: "К", + ќ: "к", + A̋: "A", + a̋: "a", + E̋: "E", + e̋: "e", + I̋: "I", + i̋: "i", + Ǹ: "N", + ǹ: "n", + Ồ: "O", + ồ: "o", + Ṑ: "O", + ṑ: "o", + Ừ: "U", + ừ: "u", + Ẁ: "W", + ẁ: "w", + Ỳ: "Y", + ỳ: "y", + Ȁ: "A", + ȁ: "a", + Ȅ: "E", + ȅ: "e", + Ȉ: "I", + ȉ: "i", + Ȍ: "O", + ȍ: "o", + Ȑ: "R", + ȑ: "r", + Ȕ: "U", + ȕ: "u", + B̌: "B", + b̌: "b", + Č̣: "C", + č̣: "c", + Ê̌: "E", + ê̌: "e", + F̌: "F", + f̌: "f", + Ǧ: "G", + ǧ: "g", + Ȟ: "H", + ȟ: "h", + J̌: "J", + ǰ: "j", + Ǩ: "K", + ǩ: "k", + M̌: "M", + m̌: "m", + P̌: "P", + p̌: "p", + Q̌: "Q", + q̌: "q", + Ř̩: "R", + ř̩: "r", + Ṧ: "S", + ṧ: "s", + V̌: "V", + v̌: "v", + W̌: "W", + w̌: "w", + X̌: "X", + x̌: "x", + Y̌: "Y", + y̌: "y", + A̧: "A", + a̧: "a", + B̧: "B", + b̧: "b", + Ḑ: "D", + ḑ: "d", + Ȩ: "E", + ȩ: "e", + Ɛ̧: "E", + ɛ̧: "e", + Ḩ: "H", + ḩ: "h", + I̧: "I", + i̧: "i", + Ɨ̧: "I", + ɨ̧: "i", + M̧: "M", + m̧: "m", + O̧: "O", + o̧: "o", + Q̧: "Q", + q̧: "q", + U̧: "U", + u̧: "u", + X̧: "X", + x̧: "x", + Z̧: "Z", + z̧: "z", + й: "и", + Й: "И", + ё: "е", + Ё: "Е", +}; diff --git a/src/shared/index.ts b/src/shared/index.ts index ec3fe9d9..5d216183 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,3 +1,4 @@ +import { charMap } from "./char-map"; import { Downloader } from "./constants"; export * from "./constants"; @@ -51,6 +52,12 @@ export const replaceUnderscoreWithSpace = (name: string) => name.replace(/_/g, " "); export const formatName = pipe( + (str) => + str.replace( + new RegExp(Object.keys(charMap).join("|"), "g"), + (match) => charMap[match] + ), + (str) => str.toLowerCase(), removeReleaseYearFromName, removeSpecialEditionFromName, replaceUnderscoreWithSpace, diff --git a/src/types/howlongtobeat.types.ts b/src/types/howlongtobeat.types.ts new file mode 100644 index 00000000..1ab7ee34 --- /dev/null +++ b/src/types/howlongtobeat.types.ts @@ -0,0 +1,14 @@ +export interface HowLongToBeatCategory { + title: string; + duration: string; + accuracy: string; +} + +export interface HowLongToBeatResult { + game_id: number; + game_name: string; +} + +export interface HowLongToBeatSearchResponse { + data: HowLongToBeatResult[]; +} diff --git a/src/types/index.ts b/src/types/index.ts index 7af2aeca..303d47ae 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -130,12 +130,6 @@ export interface UserPreferences { runAtStartup: boolean; } -export interface HowLongToBeatCategory { - title: string; - duration: string; - accuracy: string; -} - export interface Steam250Game { title: string; objectId: string; @@ -304,3 +298,4 @@ export interface GameArtifact { export * from "./steam.types"; export * from "./real-debrid.types"; export * from "./ludusavi.types"; +export * from "./howlongtobeat.types";