diff --git a/app.go b/app.go index bacdfcc..3d8639e 100644 --- a/app.go +++ b/app.go @@ -14,7 +14,7 @@ import ( ) var ( - games []models.Game + games []models.Metadata lastEmitTimestamp = time.Now().Add(-10 * time.Second) emitInterval = 150 * time.Millisecond gameIsOpen = false @@ -30,9 +30,9 @@ func NewApp() *App { return &App{} } -// startup is called when the app starts. The context is saved +// Startup is called when the app starts. The context is saved // so we can call the runtime methods -func (a *App) startup(ctx context.Context) { +func (a *App) Startup(ctx context.Context) { a.ctx = ctx inputs.Start(a.onControllerChange) provider.Update() @@ -92,11 +92,21 @@ func (a *App) StartGame(id string) { } } -func (a *App) LoadGames() []models.Game { +func (a *App) LoadGames() []models.Metadata { games = provider.GetConjureGameInfo() return games } -func (a *App) LoadImage(gameId string, imageSrc string) provider.FileBlob { - return *provider.LoadImage(gameId, imageSrc) +func (a *App) LoadGamesNewModel() []models.Game { + return []models.Game{} +} + +func (a *App) LoadImage(gameId string, imageSrc string) provider.FileBlob { + blob := provider.LoadImage(gameId, imageSrc) + + if blob == nil { + return provider.FileBlob{} + } + + return *blob } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 3dfc225..7f01a6d 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -0d0407303e68412fce4ec212f727db35 \ No newline at end of file +0439ac4c00128949ad585ea9e2f68e8e \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 045c613..07b4e28 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,38 +1,39 @@ diff --git a/frontend/src/assets/images/person.png b/frontend/src/assets/images/person.png new file mode 100644 index 0000000..b72cbce Binary files /dev/null and b/frontend/src/assets/images/person.png differ diff --git a/frontend/src/components/DevCard.vue b/frontend/src/components/DevCard.vue new file mode 100644 index 0000000..6258549 --- /dev/null +++ b/frontend/src/components/DevCard.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/GameCard.vue b/frontend/src/components/GameCard.vue index e08eadd..ba81e77 100644 --- a/frontend/src/components/GameCard.vue +++ b/frontend/src/components/GameCard.vue @@ -1,16 +1,16 @@ diff --git a/frontend/src/components/GameCarousel.vue b/frontend/src/components/GameCarousel.vue index c8fd827..9903cc0 100644 --- a/frontend/src/components/GameCarousel.vue +++ b/frontend/src/components/GameCarousel.vue @@ -5,7 +5,7 @@ diff --git a/frontend/src/components/GamePreview.vue b/frontend/src/components/GamePreview.vue index 3ac3a0e..37dda83 100644 --- a/frontend/src/components/GamePreview.vue +++ b/frontend/src/components/GamePreview.vue @@ -1,37 +1,92 @@ @@ -39,11 +94,27 @@ import { models } from "../../wailsjs/go/models"; import Game = models.Game; import LocalImage from "./LocalImage.vue"; +import { computed } from "vue"; +import DevCard from "./DevCard.vue"; +import { useAppStore } from "../stores/app-store"; +import ImageCarousel from "./ImageCarousel.vue"; -defineProps<{ +const store = useAppStore(); + +const props = defineProps<{ game: Game, }>(); +const buttonStyle = computed(() => { + const ternary = props.game?.color_scheme?.ternary ?? "#ffffff"; + const secondary = props.game?.color_scheme?.primary ?? "#100a99"; + + return { + backgroundColor: secondary, + textColor: ternary + }; +}) + defineEmits(['qr']); @@ -51,6 +122,7 @@ defineEmits(['qr']); img { transition: transform 0.2s; } + img:hover { transform: scale(1.05); } diff --git a/frontend/src/components/ImageCarousel.vue b/frontend/src/components/ImageCarousel.vue new file mode 100644 index 0000000..a1cfb7f --- /dev/null +++ b/frontend/src/components/ImageCarousel.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 49a4828..e2a9231 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -1,25 +1,27 @@ diff --git a/frontend/src/services/game-service.ts b/frontend/src/services/game-service.ts index 284d55d..2efa052 100644 --- a/frontend/src/services/game-service.ts +++ b/frontend/src/services/game-service.ts @@ -1,52 +1,130 @@ import {LoadGames} from "../../wailsjs/go/main/App"; import {models} from "../../wailsjs/go/models"; +import Metadata = models.Metadata; import Game = models.Game; +import Developer = models.Developer; -// const localGames: Game[] = [ -// { -// id: 'g1', -// title: 'Neon Rush', -// description: 'A fast-paced 2D platformer through glowing neon levels.', -// playerCount: '1', -// languages: ['English', 'French'], -// tags: ['platformer', 'neon', 'funny'], -// media: [ -// '/assets/neon-rush-1.png', -// '/assets/neon-rush-2.png' -// ], -// thumbnail: '/assets/neon-rush-thumb.png', -// links: { -// Website: 'https://neonrush.dev', -// Itch: 'https://hyperbyte.itch.io/neonrush' -// }, -// executablePath: "." -// }, -// { -// id: 'g2', -// title: 'Ghost Bakery', -// description: 'Bake spooky pastries in a haunted kitchen.', -// playerCount: '1-2', -// languages: ['English'], -// tags: ['cooking', 'funny', 'co-op'], -// media: [ -// '/assets/ghost-bakery-1.jpg' -// ], -// thumbnail: '/assets/ghost-bakery-thumb.jpg', -// links: { -// Itch: 'https://phantomforks.itch.io/ghostbakery' -// }, -// executablePath: "." -// } -// ]; +const localGames: Game[] = [ + new Game({ + id: "ddf1ab0c-d86e-442f-8fd8-cfe8a0dc0a52", + title: "Soul Shaper", + version: "2.0.0", + description: "Laissez votre imagination prendre forme en maniant le pouvoir de la création contre la seule chose qui vous sépare du paradis. Manipulez l'espace et le temps contre votre adversaire dans Soul Shaper!", + players: "1-1", + release: "2023-12-15", + modification: "2023-12-15", + executable: "game\\Soul Shaper.exe", + public_repository_link: "https://github.com/PFE033-ConjureOS/ConjureOS-SoulShaper", + genres: "action", + developers: [ + new Developer({ + name: "William Gingras" + }), + new Developer({ + name: "Yussef Shehadeh" + }), + new Developer({ + name: "Leah Fortin" + }), + new Developer({ + name: "Cameron Lamoureux" + }), + ], + thumbnail_path: "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgzNzY0Ny5wbmc=/original/r7iVIj.png", + itch_link: "https://craftelia.itch.io/gamelab2023-blood-god", + media_paths: [ + "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgyMjc0Ni5wbmc=/original/65c%2FQT.png", + "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgyMjc0Ny5wbmc=/original/4FiVFR.png", + "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgyMjc0OC5wbmc=/original/IkUG5I.png", + "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgzNzY1My5wbmc=/original/jtOMly.png" + ], + logo_path: "https://img.itch.zone/aW1nLzExODI4OTQyLnBuZw==/original/0MBnt3.png", + backgroundImagePath: "https://img.itch.zone/aW1nLzExODIyNzg4LnBuZw==/original/QVGL4L.png", + collections: "Ubi Gamelab", + color_scheme: { + primary: "#66258a", + secondary: "#80f071", + ternary: "#ffffff" + }, + }), + new Game({ + id: "random-id", + title: "Froggin' Around", + version: "2.0.0", + description: "Incarnez deux grenouilles s'échappant d'un atelier magique. Travaillez ensemble et utilisez vos langues élastiques et rebondissantes pour progresser et résoudre des énigmes.", + players: "2-2", + release: "2025-04-13", + modification: "2023-05-05", + executable: "game\\FrogginAround.exe", + genres: "action, adventure, puzzle", + developers: [ + new Developer({ + name: "Jeanne Castonguay", + role: "Designer" + }), + new Developer({ + name: "Mathieu Vézina", + role: "Developer", + link: "https://www.linkedin.com/in/mathieu-vezina/", + picture: "https://media.licdn.com/dms/image/v2/C4E03AQElTJL0iSscHg/profile-displayphoto-shrink_400_400/profile-displayphoto-shrink_400_400/0/1637863156486?e=1757548800&v=beta&t=oJQ1GfgqFx6dBgtIxS6GpsPp_eOYRGhVpAzmO1_ygr0" + }), + new Developer({ + name: "Loïc Cyr", + role: "Developer" + }), + new Developer({ + name: "Céline", + role: "Art" + }), + ], + thumbnail_path: "https://img.itch.zone/aW1nLzIwNzAwNDg1LnBuZw==/original/qGDxOj.png", + itch_link: "https://jeanne444.itch.io/froggin-around", + media_paths: [ + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcxMDI1Mi5wbmc=/original/owXY2q.png", + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcwMDgwOC5wbmc=/original/PTa96E.png", + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcwMDgwNy5wbmc=/original/PXw75v.png", + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcwMDY2OC5wbmc=/original/MwyNjV.png", + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcwMDgwOS5wbmc=/original/FYGa9e.png", + "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDcyNDg1Mi5naWY=/original/%2BLoLem.gif" + ], + logo_path: "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDg2MDc1OC5wbmc=/original/tgccKa.png", + collections: "Ubi Gamelab", + color_scheme: { + primary: "#cf2d30", + secondary: "#b3cf43", + ternary: "#ffffff" + }, + }) +]; export async function fetchGames(): Promise { - // const source = localStorage.getItem('dataSource') || 'local'; - // if (source === 'local') return localGames; + const source = localStorage.getItem('dataSource') || 'local'; + if (source === 'local') return localGames; - // TODO games should be loaded from and started from the wails/go code const games = await LoadGames(); for (const game of games) { console.log(game) } - return games; + return games.map(convertToNewFormat); +} + +function convertToNewFormat(metadata: Metadata): Game { + return new Game({ + id: metadata.Id, + title: metadata.Game, + description: metadata.Description, + collections: metadata.Collection, + thumbnail_path: metadata.ThumbnailPath, + media_paths: [metadata.ImagePath], + genres: metadata.Genres, + executable: metadata.Files, + release: metadata.Release, + modification: metadata.Modification, + version: metadata.Version, + players: metadata.Players, + public_repository_link: metadata.PublicRepositoryLink, + developers: metadata.Developers.split(",").map((dev) => new Developer({ + name: dev + })), + }); } diff --git a/frontend/src/stores/app-store.ts b/frontend/src/stores/app-store.ts index 3c439fa..cbff8bf 100644 --- a/frontend/src/stores/app-store.ts +++ b/frontend/src/stores/app-store.ts @@ -10,14 +10,15 @@ export const useAppStore = defineStore('app', { selectedTag: null as string | null, transitionDirection: 'down' as 'up' | 'down', selectedGame: null as Game | null, - selectedGameIndex: -1, + selectedGameIndex: 0, qrLink: '' as string, optionsOpen: false as boolean, - gameIsStarting: false as boolean + gameIsStarting: false as boolean, + showSidebar: false as boolean, }), getters: { filteredGames(state): Game[] { - return state.games.filter(game => game.Genres.split(",").includes(state.selectedTag ?? '')); + return state.games.filter(game => game.genres.split(",").includes(state.selectedTag ?? '')); } }, actions: { @@ -39,14 +40,11 @@ export const useAppStore = defineStore('app', { }, selectGame(index: number) { const games = this.filteredGames; - if (index >= 0 && index < games.length) { - this.selectedGameIndex = index; - this.selectedGame = games[index]; - } - else { - this.selectedGameIndex = -1; - this.selectedGame = null; - } + console.log(index) + index = mod(index, games.length); + console.log(index) + this.selectedGameIndex = index; + this.selectedGame = games[index] }, selectTag(tag: string) { this.selectedTag = tag; @@ -66,4 +64,8 @@ export const useAppStore = defineStore('app', { } } } -}); \ No newline at end of file +}); + +function mod(n, m) { + return ((n % m) + m) % m; +} \ No newline at end of file diff --git a/frontend/src/style.css b/frontend/src/style.css index f567423..535378c 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,14 +1,6 @@ @import "tailwindcss"; -html { - background-color: rgba(27, 38, 54, 1); - text-align: center; - color: white; -} - body { - margin: 0; - color: white; font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; @@ -24,5 +16,6 @@ body { #app { height: 100vh; - text-align: center; + background-color: black; + color: white; } diff --git a/frontend/src/utils/image-manager.ts b/frontend/src/utils/image-manager.ts index 45c6f12..e20cea8 100644 --- a/frontend/src/utils/image-manager.ts +++ b/frontend/src/utils/image-manager.ts @@ -5,6 +5,11 @@ export class ImageManager { static Dictionary: {[key: string]: string} = {} public static async getImage(gameId: string, src: string): Promise { + + if (src.startsWith("http")) { + return src; + } + const id = gameId + "\\" + src; console.log(gameId, src, id) if (this.Dictionary[id]) diff --git a/frontend/src/utils/key-contexts/carousel-key-context.ts b/frontend/src/utils/key-contexts/carousel-key-context.ts index e36648e..1326dee 100644 --- a/frontend/src/utils/key-contexts/carousel-key-context.ts +++ b/frontend/src/utils/key-contexts/carousel-key-context.ts @@ -6,12 +6,16 @@ export class CarouselKeyContext extends KeyContext { protected onKeyUp() { super.onKeyUp(); - this.store.moveTagUp(); + if (this.store.showSidebar) { + this.store.moveTagUp(); + } } protected onKeyDown() { super.onKeyDown(); - this.store.moveTagDown(); + if (this.store.showSidebar) { + this.store.moveTagDown(); + } } protected onKeyRight() { @@ -21,11 +25,13 @@ export class CarouselKeyContext extends KeyContext { protected onKeyLeft() { super.onKeyLeft(); - if (this.store.selectedGameIndex === 0) { + if (this.store.selectedGameIndex === 0 && this.store.showSidebar) { + console.log("fjxhfjkdshjkfd") KeyboardManager.switchContext("sidebar"); this.store.selectGame(-1); } else { + console.log("assdsds") this.store.moveGameLeft(); } } @@ -33,11 +39,23 @@ export class CarouselKeyContext extends KeyContext { protected onEscape() { super.onEscape(); this.store.optionsOpen = true; - KeyboardManager.switchContext('options'); + KeyboardManager.switchContext("options"); } protected onEnter() { super.onEnter(); - this.store.startSelectedGame(); + this.store.startSelectedGame().then(); + } + + protected onSpace() { + super.onSpace(); + if (this.store.selectedGameIndex !== -1) { + KeyboardManager.switchContext("preview"); + const playBtn = document.getElementById("btn-play"); + + if (playBtn) { + playBtn.focus(); + } + } } } \ No newline at end of file diff --git a/frontend/src/utils/key-contexts/game-preview-key-context.ts b/frontend/src/utils/key-contexts/game-preview-key-context.ts index 5e33230..d9af189 100644 --- a/frontend/src/utils/key-contexts/game-preview-key-context.ts +++ b/frontend/src/utils/key-contexts/game-preview-key-context.ts @@ -1,25 +1,86 @@ import { KeyContext } from "./key-context"; +import { KeyboardManager } from "../keyboard-manager"; export class GamePreviewKeyContext extends KeyContext { readonly name: string = "GamePreviewKeyContext"; protected onKeyUp() { super.onKeyUp(); + this.focusLastElement() } protected onKeyDown() { super.onKeyDown(); + this.focusNextElement(); + // const activeElement = document.activeElement; + // activeElement.nextSibling.parentElement.focus(); + // const tabEvent = new KeyboardEvent('keydown', { + // key: 'Tab', + // code: 'Tab', + // bubbles: true, // Allow the event to bubble up the DOM tree + // cancelable: true // Allow the event to be canceled + // }); + // + // // Dispatch the event on the desired element + // window.dispatchEvent(tabEvent); + } + + private focusNextElement() { + const focusableSelectors = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled]):not([type="hidden"])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])' + ]; + + const focusableElements = Array.from(document.querySelectorAll(focusableSelectors.join(','))) + .filter(el => (el as any).offsetParent !== null); // filter out hidden elements + + const currentIndex = focusableElements.indexOf(document.activeElement); + const nextIndex = (currentIndex + 1) % focusableElements.length; + + (focusableElements[nextIndex] as any).focus(); + } + + private focusLastElement() { + const focusableSelectors = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled]):not([type="hidden"])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])' + ]; + + const focusableElements = Array.from(document.querySelectorAll(focusableSelectors.join(','))) + .filter(el => (el as any).offsetParent !== null); // filter out hidden elements + + const currentIndex = focusableElements.indexOf(document.activeElement); + const nextIndex = (currentIndex - 1) % focusableElements.length; + + (focusableElements[nextIndex] as any).focus(); } protected onKeyRight() { super.onKeyRight(); + this.focusNextElement(); + } + + protected onKeyLeft() { + super.onKeyLeft(); + this.focusLastElement(); } protected onEnter() { super.onEnter(); + (document.activeElement as any).click(); } protected onEscape() { super.onEscape(); + (document.activeElement as any).blur(); + KeyboardManager.switchContext("carousel"); } } \ No newline at end of file diff --git a/frontend/src/utils/key-contexts/key-context.ts b/frontend/src/utils/key-contexts/key-context.ts index dce2859..8f36342 100644 --- a/frontend/src/utils/key-contexts/key-context.ts +++ b/frontend/src/utils/key-contexts/key-context.ts @@ -26,6 +26,9 @@ export abstract class KeyContext { case 'Enter': this.onEnter(); break; + case ' ': + this.onSpace(); + break; default: break; } @@ -50,6 +53,10 @@ export abstract class KeyContext { this.onEnter() } + if ((state.buttons & 0x08) !== 0) { + this.onSpace() + } + // TODO should be 0x01 when the power button will work if ((state.buttons & 0x02) !== 0) { this.onEscape() @@ -79,4 +86,8 @@ export abstract class KeyContext { protected onEnter(): void { console.log('onEnter'); } + + protected onSpace(): void { + console.log('onSpace'); + } } \ No newline at end of file diff --git a/frontend/src/utils/keyboard-manager.ts b/frontend/src/utils/keyboard-manager.ts index 6eadbae..d9f42bb 100644 --- a/frontend/src/utils/keyboard-manager.ts +++ b/frontend/src/utils/keyboard-manager.ts @@ -3,15 +3,31 @@ import { CarouselKeyContext } from "./key-contexts/carousel-key-context"; import { KeyContext } from "./key-contexts/key-context"; import { OptionsKeyContext } from "./key-contexts/options-key-context"; import { SidebarKeyContext } from "./key-contexts/sidebar-key-context"; +import { GamePreviewKeyContext } from "./key-contexts/game-preview-key-context"; export class KeyboardManager { private static current?: KeyContext; + public static loaded = false; - static switchContext(name: 'sidebar' | 'carousel' | 'options') { + static switchContext(name: 'sidebar' | 'carousel' | 'options' | 'preview') { console.log("Switching context to " + name); - if (name === 'sidebar') this.current = new SidebarKeyContext(); - else if (name === 'carousel') this.current = new CarouselKeyContext(); - else if (name === 'options') this.current = new OptionsKeyContext(); + switch (name) { + case 'carousel': + this.current = new CarouselKeyContext(); + break; + case 'sidebar': + this.current = new SidebarKeyContext(); + break; + case 'preview': + this.current = new GamePreviewKeyContext(); + break; + case 'options': + this.current = new OptionsKeyContext(); + break; + default: + console.log("Unknown context key " + name); + break; + } } static handle(event: KeyboardEvent) { diff --git a/frontend/src/utils/use-keyboard-navigation.ts b/frontend/src/utils/use-keyboard-navigation.ts index 666d59f..2141598 100644 --- a/frontend/src/utils/use-keyboard-navigation.ts +++ b/frontend/src/utils/use-keyboard-navigation.ts @@ -1,14 +1,14 @@ -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted } from 'vue'; import { KeyboardManager } from "./keyboard-manager"; import { EventsOn } from "../../wailsjs/runtime"; export function useKeyboardNavigation(): void { onMounted(() => { - EventsOn("controller_change", KeyboardManager.handleState.bind(KeyboardManager)); - window.addEventListener('keydown', KeyboardManager.handle.bind(KeyboardManager)); - }); - - onBeforeUnmount(() => { - window.removeEventListener('keydown', KeyboardManager.handle); + if (!KeyboardManager.loaded) { + console.log("Loading inputs") + EventsOn("controller_change", KeyboardManager.handleState.bind(KeyboardManager)); + window.addEventListener('keydown', KeyboardManager.handle.bind(KeyboardManager)); + KeyboardManager.loaded = true; + } }); } diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 5b27953..952543f 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -3,7 +3,9 @@ import {models} from '../models'; import {provider} from '../models'; -export function LoadGames():Promise>; +export function LoadGames():Promise>; + +export function LoadGamesNewModel():Promise>; export function LoadImage(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 2b66092..aa6e549 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -6,6 +6,10 @@ export function LoadGames() { return window['go']['main']['App']['LoadGames'](); } +export function LoadGamesNewModel() { + return window['go']['main']['App']['LoadGamesNewModel'](); +} + export function LoadImage(arg1, arg2) { return window['go']['main']['App']['LoadImage'](arg1, arg2); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 5ec11da..a650e30 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,6 +1,108 @@ export namespace models { + export class ColorScheme { + primary: string; + secondary: string; + ternary: string; + + static createFrom(source: any = {}) { + return new ColorScheme(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.primary = source["primary"]; + this.secondary = source["secondary"]; + this.ternary = source["ternary"]; + } + } + export class Developer { + id: string; + name: string; + link: string; + picture: string; + role: string; + + static createFrom(source: any = {}) { + return new Developer(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.name = source["name"]; + this.link = source["link"]; + this.picture = source["picture"]; + this.role = source["role"]; + } + } export class Game { + id: string; + title: string; + description: string; + version: string; + players: string; + release: string; + modification: string; + public_repository_link: string; + itch_link: string; + genres: string; + collections: string; + executable: string; + thumbnail_path: string; + logo_path: string; + backgroundImagePath: string; + media_paths: string[]; + audio_path: string; + developers: Developer[]; + color_scheme: ColorScheme; + + static createFrom(source: any = {}) { + return new Game(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.title = source["title"]; + this.description = source["description"]; + this.version = source["version"]; + this.players = source["players"]; + this.release = source["release"]; + this.modification = source["modification"]; + this.public_repository_link = source["public_repository_link"]; + this.itch_link = source["itch_link"]; + this.genres = source["genres"]; + this.collections = source["collections"]; + this.executable = source["executable"]; + this.thumbnail_path = source["thumbnail_path"]; + this.logo_path = source["logo_path"]; + this.backgroundImagePath = source["backgroundImagePath"]; + this.media_paths = source["media_paths"]; + this.audio_path = source["audio_path"]; + this.developers = this.convertValues(source["developers"], Developer); + this.color_scheme = this.convertValues(source["color_scheme"], ColorScheme); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class Metadata { Collection: string; Launch: string; Id: string; @@ -16,10 +118,9 @@ export namespace models { PublicRepositoryLink: string; Genres: string; Developers: string; - Title: string; static createFrom(source: any = {}) { - return new Game(source); + return new Metadata(source); } constructor(source: any = {}) { @@ -39,7 +140,6 @@ export namespace models { this.PublicRepositoryLink = source["PublicRepositoryLink"]; this.Genres = source["Genres"]; this.Developers = source["Developers"]; - this.Title = source["Title"]; } } diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts index 4445dac..94778df 100644 --- a/frontend/wailsjs/runtime/runtime.d.ts +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): void; +export function WindowSetSize(width: number, height: number): Promise; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/go.mod b/go.mod index aa26443..a42c190 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.5 require ( github.com/karalabe/hid v1.0.0 - github.com/wailsapp/wails/v2 v2.10.1 + github.com/wailsapp/wails/v2 v2.9.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 58e26dc..9690a5f 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyT github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns= -github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY= +github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k= +github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/lib/config/config.go b/lib/config/config.go index 6c6471c..22e5bed 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "os" "path/filepath" "runtime" @@ -21,13 +20,11 @@ func GetDefaultConjureOsDirectory() string { if err != nil { panic(err) } - fmt.Println("User config dir:", configDir) cacheDir, err := os.UserCacheDir() if err != nil { panic(err) } - fmt.Println("User cache dir:", cacheDir) switch runtime.GOOS { // I want the data to be store in LocalAppData not RoamingAppData diff --git a/lib/models/game.go b/lib/models/game.go index 69b5466..f72583b 100644 --- a/lib/models/game.go +++ b/lib/models/game.go @@ -1,6 +1,6 @@ package models -type Game struct { +type Metadata struct { Collection string Launch string Id string @@ -16,5 +16,40 @@ type Game struct { PublicRepositoryLink string Genres string Developers string - Title string +} + +type Game struct { + Id string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Version string `json:"version"` + Players string `json:"players"` + Release string `json:"release"` + Modification string `json:"modification"` + PublicRepositoryLink string `json:"public_repository_link"` + ItchLink string `json:"itch_link"` + Genres string `json:"genres"` + Collections string `json:"collections"` + Executable string `json:"executable"` + ThumbnailPath string `json:"thumbnail_path"` + LogoPath string `json:"logo_path"` + BackgroundImagePath string `json:"backgroundImagePath"` + MediaPaths []string `json:"media_paths"` + AudioPath string `json:"audio_path"` + Developers []Developer `json:"developers"` + ColorScheme ColorScheme `json:"color_scheme"` +} + +type Developer struct { + Id string `json:"id"` + Name string `json:"name"` + Link string `json:"link"` + Picture string `json:"picture"` + Role string `json:"role"` +} + +type ColorScheme struct { + Primary string `json:"primary"` + Secondary string `json:"secondary"` + Ternary string `json:"ternary"` } diff --git a/lib/provider/file-provider.go b/lib/provider/file-provider.go index 3e08406..bcc1a6a 100644 --- a/lib/provider/file-provider.go +++ b/lib/provider/file-provider.go @@ -12,7 +12,7 @@ func LoadImage(gameId string, imageSrc string) *FileBlob { imagePath := filepath.Join(config.GetDefaultConjureGamesDirectory(), gameId, imageSrc) blob, err := GetFileBlob(imagePath) if err != nil { - panic(err) + return nil } return blob diff --git a/lib/provider/provider.go b/lib/provider/provider.go index 38ae985..a3329ec 100644 --- a/lib/provider/provider.go +++ b/lib/provider/provider.go @@ -160,7 +160,7 @@ func extractZipsInFolder(folder string) error { return nil } -func ExtractGame(game models.Game) string { +func ExtractGame(game models.Metadata) string { info, err := os.Stat(filepath.Join(config.GetDefaultConjureGamesDirectory(), game.Id)) if err != nil || !info.IsDir() { gamePath := filepath.Join(config.GetDefaultConjureGamesDirectory(), fmt.Sprintf("%s.conj", game.Id)) @@ -180,7 +180,7 @@ func ExtractGame(game models.Game) string { return gamePath } -func GetConjureGameInfo() []models.Game { +func GetConjureGameInfo() []models.Metadata { gamePath := config.GetDefaultConjureGamesDirectory() @@ -189,7 +189,7 @@ func GetConjureGameInfo() []models.Game { log.Fatal(err) } - var games []models.Game + var games []models.Metadata for _, e := range entries { if e.IsDir() { @@ -246,12 +246,12 @@ func printIndentedPath(path string) { } } -func readFolder(entry fs.DirEntry, path string) []models.Game { +func readFolder(entry fs.DirEntry, path string) []models.Metadata { newPath := path + "/" + entry.Name() entries, err := os.ReadDir(newPath) check(err) - var games []models.Game + var games []models.Metadata for _, e := range entries { if e.IsDir() { @@ -268,14 +268,14 @@ func readFolder(entry fs.DirEntry, path string) []models.Game { return games } -func parseGameInfoFromFile(path string) models.Game { +func parseGameInfoFromFile(path string) models.Metadata { data, err := os.ReadFile(path) check(err) return parseGameInfo(data) } -func parseGameInfo(data []byte) models.Game { - game := models.Game{} +func parseGameInfo(data []byte) models.Metadata { + game := models.Metadata{} err := yaml.Unmarshal(data, &game) check(err) return game diff --git a/main.go b/main.go index 2c5465e..c476859 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,10 @@ package main import ( "embed" + "fmt" + "net/http" + "os" + "strings" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -11,6 +15,27 @@ import ( //go:embed all:frontend/dist var assets embed.FS +type FileLoader struct { + http.Handler +} + +func NewFileLoader() *FileLoader { + return &FileLoader{} +} + +func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) { + var err error + requestedFilename := strings.TrimPrefix(req.URL.Path, "/") + println("Requesting file:", requestedFilename) + fileData, err := os.ReadFile(requestedFilename) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + res.Write([]byte(fmt.Sprintf("Could not load file %s", requestedFilename))) + } + + res.Write(fileData) +} + func main() { // Create an instance of the app structure app := NewApp() @@ -22,10 +47,10 @@ func main() { Height: 768, Fullscreen: true, AssetServer: &assetserver.Options{ - Assets: assets, + Assets: assets, + Handler: NewFileLoader(), }, - BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - OnStartup: app.startup, + OnStartup: app.Startup, Bind: []interface{}{ app, },