Merge pull request 'tristan-wip' (#2) from tristan-wip into main
Reviewed-on: #2
This commit is contained in:
commit
bc12f855ce
106
app.go
106
app.go
@ -1,15 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"conjure-os/lib/inputs"
|
||||
"conjure-os/lib/models"
|
||||
"conjure-os/lib/provider"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"conjure-os/lib/inputs"
|
||||
"conjure-os/lib/provider"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
games []models.Metadata
|
||||
lastEmitTimestamp = time.Now().Add(-10 * time.Second)
|
||||
emitInterval = 150 * time.Millisecond
|
||||
gameIsOpen = false
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
@ -20,23 +30,93 @@ 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()
|
||||
inputs.Start(a.onControllerChange)
|
||||
provider.Update()
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) string {
|
||||
runtime.LogInfo(a.ctx, "Test : "+name)
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
func (a *App) onControllerChange(state inputs.ControllerState) {
|
||||
now := time.Now()
|
||||
|
||||
if now.Sub(lastEmitTimestamp) >= emitInterval && !gameIsOpen {
|
||||
if state.Buttons != 0 {
|
||||
for _, button := range inputs.ConjureControllerButtons {
|
||||
if state.Buttons&(1<<button) != 0 {
|
||||
fmt.Printf("Button %s pressed\n", (button).String())
|
||||
}
|
||||
}
|
||||
fmt.Printf("Button was pressed! %d\n", state.Buttons)
|
||||
}
|
||||
|
||||
if state.Joystick.X != 127 || state.Joystick.Y != 127 {
|
||||
fmt.Printf("Joystick moved! %d - %d\n", state.Joystick.X, state.Joystick.Y)
|
||||
}
|
||||
|
||||
fmt.Printf("Joystick: X=%d Y=%d Buttons=%08b\n", state.Joystick.X, state.Joystick.Y, state.Buttons)
|
||||
runtime.EventsEmit(a.ctx, "controller_change", state)
|
||||
lastEmitTimestamp = now
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SelectGame(id string) {
|
||||
func (a *App) StartGame(id string) {
|
||||
found := false
|
||||
fmt.Println(id)
|
||||
for _, game := range games {
|
||||
fmt.Println(game.Id)
|
||||
if game.Id == id {
|
||||
found = true
|
||||
gamePath := provider.ExtractGame(game)
|
||||
cmd := exec.Command(gamePath)
|
||||
gameIsOpen = true
|
||||
|
||||
// Optional: attach current terminal's std streams
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
// Start the process
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Println("Failed to start:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Process started with PID %d\n", cmd.Process.Pid)
|
||||
|
||||
// Optional: wait for it to finish
|
||||
if err := cmd.Wait(); err != nil {
|
||||
fmt.Println("Process exited with error:", err)
|
||||
} else {
|
||||
fmt.Println("Process exited successfully.")
|
||||
}
|
||||
gameIsOpen = false
|
||||
}
|
||||
}
|
||||
fmt.Printf("Found %b", found)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (a *App) LoadGames() []provider.Game {
|
||||
return provider.ObtainConjureGameInfo()
|
||||
func (a *App) LoadGames() []models.Metadata {
|
||||
games = provider.GetConjureGameInfo()
|
||||
return games
|
||||
}
|
||||
|
||||
func (a *App) LoadGamesNewModel() []models.Game {
|
||||
return []models.Game{}
|
||||
}
|
||||
|
||||
func (a *App) Log(message string) {
|
||||
fmt.Println(message)
|
||||
}
|
||||
|
||||
func (a *App) LoadImage(gameId string, imageSrc string) provider.FileBlob {
|
||||
blob := provider.LoadImage(gameId, imageSrc)
|
||||
|
||||
if blob == nil {
|
||||
return provider.FileBlob{}
|
||||
}
|
||||
|
||||
return *blob
|
||||
}
|
||||
|
||||
BIN
build/.DS_Store
vendored
Normal file
BIN
build/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 36 KiB |
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.18.10",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
@ -512,6 +513,27 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@iconify/vue": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-5.0.0.tgz",
|
||||
"integrity": "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@iconify/types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyberalien"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.18.10",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
|
||||
@ -1 +1 @@
|
||||
1f77c5bc2ac4189b53ca32c331845b25
|
||||
063583417fe9e58bc0e2d6b154be7c49
|
||||
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="flex h-screen w-screen bg-black text-white">
|
||||
<div class="flex h-screen w-screen text-white">
|
||||
<div class="flex flex-1 overflow-hidden" :style="backgroundStyle">
|
||||
<!-- Sidebar -->
|
||||
<Sidebar
|
||||
v-if="showSidebar"
|
||||
:tags="tags"
|
||||
:selectedTag="selectedTag"
|
||||
@selectTag="store.selectTag"
|
||||
@ -10,53 +12,70 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex flex-col flex-1 overflow-hidden">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<GamePreview
|
||||
:game="selectedGame"
|
||||
@qr="store.showQr"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<GameCarousel
|
||||
:games="store.filteredGames"
|
||||
:selectedGame="selectedGame"
|
||||
:selectedTag="selectedTag"
|
||||
:direction="transitionDirection"
|
||||
@selectGame="store.selectGame"
|
||||
class="mb-2"
|
||||
/>
|
||||
<CurrentActionsHelp />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OptionsModal v-if="optionsOpen" @close="optionsOpen = false" />
|
||||
<QrModal v-if="qrLink" :link="qrLink" @close="qrLink = ''" />
|
||||
<OptionsModal v-if="optionsOpen" @close="optionsOpen = false"/>
|
||||
<QrModal v-if="qrLink" :link="qrLink" @close="qrLink = ''"/>
|
||||
<LoadingModal v-if="gameIsStarting"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { onMounted, computed } from 'vue';
|
||||
import Sidebar from './components/Sidebar.vue';
|
||||
import GamePreview from './components/GamePreview.vue';
|
||||
import GameCarousel from './components/GameCarousel.vue';
|
||||
import OptionsModal from './components/OptionsModal.vue';
|
||||
import QrModal from './components/QrModal.vue';
|
||||
import { useKeyboardNavigation } from './utils/use-keyboard-navigation';
|
||||
import { fetchGames } from './services/game-service';
|
||||
import LoadingModal from './components/LoadingModal.vue';
|
||||
import { useAppStore } from "./stores/app-store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { KeyboardManager } from "./utils/keyboard-manager";
|
||||
import CurrentActionsHelp from "./components/CurrentActionsHelp.vue";
|
||||
|
||||
const store = useAppStore();
|
||||
const { selectedTag, selectedGame, tags, games, transitionDirection, qrLink } = storeToRefs(store);
|
||||
|
||||
const optionsOpen = ref(false);
|
||||
const {
|
||||
selectedTag,
|
||||
selectedGame,
|
||||
tags,
|
||||
transitionDirection,
|
||||
qrLink,
|
||||
gameIsStarting,
|
||||
optionsOpen,
|
||||
showSidebar
|
||||
} = storeToRefs(store);
|
||||
|
||||
onMounted(async () => {
|
||||
games.value = await fetchGames();
|
||||
tags.value = [...new Set(games.value.flatMap(game => game.tags))];
|
||||
selectedTag.value = tags.value[0];
|
||||
selectedGame.value = store.filteredGames[0];
|
||||
await store.loadGames();
|
||||
});
|
||||
|
||||
KeyboardManager.switchContext("sidebar")
|
||||
useKeyboardNavigation();
|
||||
// Create the gradient background style
|
||||
const backgroundStyle = computed(() => {
|
||||
const primary = selectedGame?.value?.colorScheme?.primary ?? "#4d97f8";
|
||||
const secondary = selectedGame?.value?.colorScheme?.secondary ?? "#100a7d";
|
||||
|
||||
const backgroundImage = `linear-gradient(135deg, ${primary}, ${secondary})`;
|
||||
// const backgroundImage = !!bgImage ? `url(${bgImage})` : `linear-gradient(135deg, ${primary}, ${secondary})`;
|
||||
|
||||
return {
|
||||
backgroundImage
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
BIN
frontend/src/assets/images/person.png
Normal file
BIN
frontend/src/assets/images/person.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
51
frontend/src/components/CurrentActionsHelp.vue
Normal file
51
frontend/src/components/CurrentActionsHelp.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="bg-black min-h-10">
|
||||
<div v-if="currentActions" class="flex justify-around w-full h-full items-center px-4">
|
||||
<span v-for="tip in currentActions"> {{ tip }} </span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span>Chungus</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useAppStore } from "../stores/app-store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
|
||||
const store = useAppStore();
|
||||
const {
|
||||
currentInputDevice,
|
||||
currentAvailableActions,
|
||||
} = storeToRefs(store);
|
||||
|
||||
const currentActions = computed(() => {
|
||||
const { order, ...rest } = currentAvailableActions.value as any;
|
||||
|
||||
console.log(order, rest);
|
||||
|
||||
if (!order) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const possibleControllers = order.split(",");
|
||||
const index = possibleControllers.findIndex(controller => controller === currentInputDevice.value);
|
||||
const list = [];
|
||||
console.log(index);
|
||||
console.log(rest);
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
console.log(key, value);
|
||||
const split = key.split(",");
|
||||
const myKey = split[index];
|
||||
list.push(`${myKey}: ${rest[key]}`);
|
||||
}
|
||||
return list;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
34
frontend/src/components/CustomCard.vue
Normal file
34
frontend/src/components/CustomCard.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
|
||||
<div class="dev-card flex border-1 rounded p-2 border-gray-300 gap-3 items-center" :tabindex="!!tabbable ? 10 : -1" @click="$emit('click')">
|
||||
<div>
|
||||
<img v-if="!!picture && !useIcon" :src="picture" alt="picture" class="w-14 rounded-full" />
|
||||
<Icon v-else-if="!!picture" :icon="picture" class="w-10 h-10"/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm">{{ title }}</span>
|
||||
<span>{{ content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { models } from "../../wailsjs/go/models";
|
||||
import Developer = models.Developer;
|
||||
|
||||
defineProps<{
|
||||
title: string,
|
||||
content: string,
|
||||
picture: string,
|
||||
useIcon: boolean,
|
||||
tabbable: boolean,
|
||||
}>();
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
28
frontend/src/components/DevCard.vue
Normal file
28
frontend/src/components/DevCard.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
|
||||
<div class="dev-card flex border-1 rounded p-2 border-gray-300 gap-3" tabindex="0" @click="$emit('qr', dev.link)">
|
||||
<img v-if="dev.picture" :src="dev.picture" alt="picture" class="w-14 rounded-full" />
|
||||
<Icon v-else icon="fluent:person-circle-24-filled" class="w-14 h-14"/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span>{{ dev.name }}</span>
|
||||
<span>{{ dev.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { models } from "../../wailsjs/go/models";
|
||||
import Developer = models.Developer;
|
||||
|
||||
defineProps<{
|
||||
dev: Developer,
|
||||
}>();
|
||||
|
||||
defineEmits(['qr']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
31
frontend/src/components/GameCard.vue
Normal file
31
frontend/src/components/GameCard.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div
|
||||
:key="game.id"
|
||||
:class="[
|
||||
'transition-transform transform cursor-pointer rounded-lg overflow-hidden border-4',
|
||||
selected ? 'scale-110 border-blue-500' : 'scale-100 border-transparent'
|
||||
]"
|
||||
>
|
||||
<LocalImage
|
||||
:src="game.thumbnailPath"
|
||||
class="h-32 w-48 object-cover"
|
||||
:alt="game.title"
|
||||
:gameId="game.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { models } from "../../wailsjs/go/models";
|
||||
import Game = models.Game;
|
||||
import LocalImage from "./LocalImage.vue";
|
||||
|
||||
defineProps<{
|
||||
game: Game
|
||||
selected: boolean
|
||||
}>()
|
||||
</script>
|
||||
@ -1,23 +1,14 @@
|
||||
<template>
|
||||
<div class="relative h-[170px]">
|
||||
<Transition :name="`carousel-${direction}`" mode="out-in">
|
||||
<div :key="selectedTag" class="w-full py-4 px-6 flex overflow-x-auto space-x-4 items-end transition-inner">
|
||||
<div
|
||||
<div :key="selectedTag" class="w-full py-4 px-6 flex overflow-hidden space-x-4 items-end transition-inner">
|
||||
<GameCard
|
||||
v-for="game in games"
|
||||
:key="game.id"
|
||||
:game="game"
|
||||
:selected="game.id === selectedGame?.id"
|
||||
@click="$emit('selectGame', game)"
|
||||
:class="[
|
||||
'transition-transform transform cursor-pointer rounded-lg overflow-hidden border-4',
|
||||
game.id === selectedGame.id ? 'scale-110 border-blue-500' : 'scale-100 border-transparent'
|
||||
]"
|
||||
>
|
||||
<img
|
||||
:src="game.thumbnail"
|
||||
class="h-32 w-48 object-cover"
|
||||
:alt="game.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Give the whole carousel a key based on selectedTag to trigger transition -->
|
||||
<!-- <div :key="selectedTag" class="flex gap-4 w-full transition-inner">-->
|
||||
<!-- <GameCard-->
|
||||
@ -33,7 +24,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Game } from "../models/game";
|
||||
|
||||
import { models } from "../../wailsjs/go/models";
|
||||
import Game = models.Game;
|
||||
import GameCard from "./GameCard.vue";
|
||||
|
||||
defineProps<{
|
||||
games: Game[],
|
||||
|
||||
@ -1,41 +1,131 @@
|
||||
<template>
|
||||
<div class="p-6 h-full overflow-auto">
|
||||
<div v-if="game" class="space-y-4">
|
||||
<h1 class="text-4xl font-bold">{{ game.title }}</h1>
|
||||
<div class="text-sm text-gray-400">{{ game.genre }} - {{ game.players }} players</div>
|
||||
<p class="text-lg">{{ game.description }}</p>
|
||||
<div id="preview" class="p-6 h-full w-full overflow-auto flex flex-col items-center">
|
||||
<div v-if="game" class="space-y-4 flex flex-col items-center w-full max-w-400">
|
||||
<img v-if="game.logoPath" :src="game.logoPath" :alt="game.title" class="h-72"/>
|
||||
<h1 v-else class="text-4xl font-bold py-8">{{ game.title }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<img
|
||||
v-for="(img, index) in game.media"
|
||||
:key="index"
|
||||
:src="img"
|
||||
class="rounded-lg border border-gray-600 max-h-48 object-cover"
|
||||
<!-- Actual carousel pls -->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <LocalImage-->
|
||||
<!-- v-for="(img, index) in game.media_paths"-->
|
||||
<!-- :key="index"-->
|
||||
<!-- :src="img"-->
|
||||
<!-- :alt="img"-->
|
||||
<!-- :gameId="game.id"-->
|
||||
<!-- class="rounded-lg border border-gray-600 max-h-48 object-cover"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="flex justify-between w-full space-y-1 gap-6">
|
||||
<ImageCarousel
|
||||
v-if="game.mediaPaths"
|
||||
:gameId="game.id"
|
||||
:links="game.mediaPaths"
|
||||
class="basis-3/4 h-full"
|
||||
/>
|
||||
<!-- <LocalImage-->
|
||||
<!-- v-for="(img, index) in [game.media_paths[0]]"-->
|
||||
<!-- :key="index"-->
|
||||
<!-- :src="img"-->
|
||||
<!-- :alt="img"-->
|
||||
<!-- :gameId="game.id"-->
|
||||
<!-- class="rounded-lg border border-gray-600 max-h-48 object-cover basis-1/4"/>-->
|
||||
|
||||
<div class="flex flex-col basis-1/4 gap-2">
|
||||
<CustomCard title="Number of players" :content="game.players + ' players'" :tabbable=false :use-icon=true picture="fluent:person-24-filled" />
|
||||
<CustomCard title="Genres" :content="game.genres.split(',').map(s => toUpperCamelCase(s.trim())).join(', ')" :tabbable=false :use-icon=true picture="fluent:apps-list-detail-24-filled" />
|
||||
<CustomCard title="Collections" :content="game.collections" :tabbable=false :use-icon=true picture="fluent:collections-empty-24-filled" />
|
||||
<CustomCard title="Updated" :content="game.modification" :tabbable=false :use-icon=true picture="fluent:arrow-upload-24-filled" />
|
||||
<CustomCard title="Release" :content="game.release" :tabbable=false :use-icon=true picture="fluent:clock-24-filled" />
|
||||
<CustomCard title="Version" :content="game.version" :tabbable=false :use-icon=true picture="fluent:tag-24-filled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div><strong>Languages:</strong> {{ game.languages.join(', ') }}</div>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="space-y-2 w-full flex items-start">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="(link, name) in game.links"
|
||||
:key="name"
|
||||
class="bg-blue-600 px-3 py-1 rounded hover:bg-blue-500"
|
||||
@click="$emit('qr', link)"
|
||||
v-if="game.executable"
|
||||
id="btn-play"
|
||||
key="play"
|
||||
tabindex="0"
|
||||
:style="buttonStyle"
|
||||
@click="store.startSelectedGame()"
|
||||
class="bg-blue-600 px-6 py-2 rounded hover:bg-blue-500 rounded-full text-2xl"
|
||||
>
|
||||
{{ name }} 🔗
|
||||
Play
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="game.publicRepositoryLink"
|
||||
id="btn-repo"
|
||||
key="repo"
|
||||
tabindex="0"
|
||||
:style="buttonStyle"
|
||||
@click="$emit('qr', game.publicRepositoryLink)"
|
||||
class="bg-blue-600 px-6 py-2 rounded hover:bg-blue-500 rounded-full text-2xl"
|
||||
>
|
||||
Repo
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="game.itchLink"
|
||||
id="btn-itch"
|
||||
key="itch"
|
||||
tabindex="0"
|
||||
:style="buttonStyle"
|
||||
@click="$emit('qr', game.itchLink)"
|
||||
class="bg-blue-600 px-6 py-2 rounded hover:bg-blue-500 rounded-full text-2xl"
|
||||
>
|
||||
Itch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-lg w-full wrap min-h-15">{{ game.description }}</div>
|
||||
|
||||
<div class="flex w-full items-start">
|
||||
<div class="flex gap-3 flex-wrap max-w-full">
|
||||
<DevCard
|
||||
class="min-w-60"
|
||||
v-for="dev in game.developers"
|
||||
:dev="dev"
|
||||
@qr="(value) => $emit('qr', value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="h-full flex flex-col items-center justify-center text-gray-500">
|
||||
<h1 class="text-3xl font-bold">No game selected</h1>
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500">No game selected</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
game: Object,
|
||||
});
|
||||
import { models } from "../../wailsjs/go/models";
|
||||
import Game = models.Game;
|
||||
import { computed } from "vue";
|
||||
import DevCard from "./DevCard.vue";
|
||||
import { useAppStore } from "../stores/app-store";
|
||||
import ImageCarousel from "./ImageCarousel.vue";
|
||||
import CustomCard from "./CustomCard.vue";
|
||||
import { toUpperCamelCase } from "../utils/string-utils";
|
||||
|
||||
const store = useAppStore();
|
||||
|
||||
const props = defineProps<{
|
||||
game: Game,
|
||||
}>();
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const ternary = props.game?.colorScheme?.ternary ?? "#ffffff";
|
||||
const secondary = props.game?.colorScheme?.primary ?? "#100a99";
|
||||
|
||||
return {
|
||||
backgroundColor: secondary,
|
||||
textColor: ternary
|
||||
};
|
||||
})
|
||||
|
||||
defineEmits(['qr']);
|
||||
</script>
|
||||
@ -44,6 +134,7 @@ defineEmits(['qr']);
|
||||
img {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
img:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
67
frontend/src/components/ImageCarousel.vue
Normal file
67
frontend/src/components/ImageCarousel.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<LocalImage v-if="imag" :src="imag" :alt="imag" :gameId="gameId" class="rounded-lg border border-gray-600 max-h-124 w-full h-full object-cover" />
|
||||
<div class="flex justify-center">
|
||||
<div class="flex gap-2 relative top-1 points bg-black items-center justify-center px-2 py-1 rounded-full opacity-75">
|
||||
<div
|
||||
v-for="media in links"
|
||||
:class="media === imag ? 'bg-white w-2 h-2 rounded-full opacity-100' : 'bg-white w-1 h-1 rounded-full opacity-100'"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import LocalImage from "./LocalImage.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
links: string[],
|
||||
gameId: string
|
||||
}>();
|
||||
|
||||
const index = ref(0)
|
||||
const imag = computed(() => {
|
||||
if (index.value < (props.links?.length ?? -1)) {
|
||||
return props.links[index.value];
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
watch(() => [props.gameId], async () => {
|
||||
index.value = -1;
|
||||
changeImage();
|
||||
}, { immediate: true });
|
||||
|
||||
function delay(time: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
function changeImage() {
|
||||
index.value = (index.value + 1) % props.links?.length;
|
||||
}
|
||||
|
||||
function clock() {
|
||||
delay(3000).then(() => {
|
||||
changeImage();
|
||||
clock();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
clock();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.points {
|
||||
top: -25px;
|
||||
}
|
||||
|
||||
</style>
|
||||
13
frontend/src/components/LoadingModal.vue
Normal file
13
frontend/src/components/LoadingModal.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50">
|
||||
<div class="bg-white text-black p-6 rounded-lg text-center max-w-sm w-full">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
27
frontend/src/components/LocalImage.vue
Normal file
27
frontend/src/components/LocalImage.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<img
|
||||
:src="blobUrl"
|
||||
:alt="alt"
|
||||
>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { ImageService } from "../services/image-service";
|
||||
|
||||
const props = defineProps<{
|
||||
gameId: string
|
||||
src: string
|
||||
alt: string
|
||||
}>();
|
||||
|
||||
const blobUrl = ref<string | null>(null);
|
||||
|
||||
watch(() => [props.src, props.gameId], async ([newUrl, key]) => {
|
||||
blobUrl.value = await ImageService.getImage(key, newUrl);
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50">
|
||||
<div class="fixed inset-0 bg-color flex items-center justify-center z-50">
|
||||
<div class="bg-white text-black p-6 rounded-lg w-full max-w-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Options</h2>
|
||||
|
||||
@ -40,3 +40,9 @@ function save() {
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-color {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,25 +1,27 @@
|
||||
<template>
|
||||
<div class="w-20 flex flex-col items-center bg-gray-900 py-4">
|
||||
<div class="w-20 flex flex-col items-center py-4">
|
||||
<div class="flex-1 space-y-4">
|
||||
<button
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
:class="[
|
||||
'w-12 h-12 rounded-full flex items-center justify-center',
|
||||
tag === selectedTag ? 'bg-blue-500' : 'bg-gray-700 hover:bg-gray-600'
|
||||
]"
|
||||
tabindex="-1"
|
||||
@click="$emit('selectTag', tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<div
|
||||
class="mt-auto w-12 h-12 bg-gray-600 hover:bg-gray-500 rounded-full flex items-center justify-center"
|
||||
tabindex="-1"
|
||||
@click="$emit('openOptions')"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
56
frontend/src/inputs/input-manager.ts
Normal file
56
frontend/src/inputs/input-manager.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { ControllerState } from "../models/controller-state";
|
||||
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";
|
||||
import { EventsOn } from "../../wailsjs/runtime";
|
||||
import { log } from "../services/logger-service";
|
||||
|
||||
export class InputManager {
|
||||
private static current?: KeyContext;
|
||||
public static loaded = false;
|
||||
public static loadedCount = 0;
|
||||
|
||||
static switchContext(name: 'sidebar' | 'carousel' | 'options' | 'preview') {
|
||||
console.log("Switching context to " + name);
|
||||
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;
|
||||
}
|
||||
this.current?.setAvailableActions();
|
||||
}
|
||||
|
||||
static handle(event: KeyboardEvent) {
|
||||
this.current?.handleKey(event);
|
||||
}
|
||||
|
||||
static handleState(controllerState: ControllerState) {
|
||||
this.current?.handleState(controllerState);
|
||||
}
|
||||
|
||||
static bind() {
|
||||
console.log("Trying to load inputs")
|
||||
if (!InputManager.loaded) {
|
||||
log("Loading inputs");
|
||||
EventsOn("controller_change", InputManager.handleState.bind(InputManager));
|
||||
window.addEventListener('keydown', InputManager.handle.bind(InputManager));
|
||||
InputManager.loaded = true;
|
||||
InputManager.loadedCount += 1;
|
||||
log(InputManager.loadedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
frontend/src/inputs/key-contexts/carousel-key-context.ts
Normal file
78
frontend/src/inputs/key-contexts/carousel-key-context.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { KeyContext } from "./key-context";
|
||||
import { InputManager } from "../input-manager";
|
||||
|
||||
export class CarouselKeyContext extends KeyContext {
|
||||
readonly name: string = "CarouselContext";
|
||||
|
||||
protected onKeyUp() {
|
||||
super.onKeyUp();
|
||||
if (this.store.showSidebar) {
|
||||
this.store.moveTagUp();
|
||||
}
|
||||
}
|
||||
|
||||
protected onKeyDown() {
|
||||
super.onKeyDown();
|
||||
if (this.store.showSidebar) {
|
||||
this.store.moveTagDown();
|
||||
}
|
||||
}
|
||||
|
||||
protected onKeyRight() {
|
||||
super.onKeyRight();
|
||||
this.store.moveGameRight();
|
||||
}
|
||||
|
||||
protected onKeyLeft() {
|
||||
super.onKeyLeft();
|
||||
if (this.store.selectedGameIndex === 0 && this.store.showSidebar) {
|
||||
InputManager.switchContext("sidebar");
|
||||
this.store.selectGame(-1);
|
||||
}
|
||||
else {
|
||||
this.store.moveGameLeft();
|
||||
}
|
||||
}
|
||||
|
||||
protected onEscape() {
|
||||
super.onEscape();
|
||||
this.store.optionsOpen = true;
|
||||
InputManager.switchContext("options");
|
||||
}
|
||||
|
||||
protected onEnter() {
|
||||
super.onEnter();
|
||||
this.store.startSelectedGame().then();
|
||||
}
|
||||
|
||||
protected onSpace() {
|
||||
super.onSpace();
|
||||
if (this.store.selectedGameIndex !== -1) {
|
||||
InputManager.switchContext("preview");
|
||||
const playBtn = document.getElementById("btn-play");
|
||||
const repoBtn = document.getElementById("btn-repo");
|
||||
const itchBtn = document.getElementById("btn-itch");
|
||||
const preview = document.getElementById("preview");
|
||||
|
||||
if (playBtn) {
|
||||
playBtn.focus();
|
||||
} else if (repoBtn) {
|
||||
repoBtn.focus();
|
||||
} else if (itchBtn) {
|
||||
itchBtn.focus();
|
||||
} else {
|
||||
preview.focus();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public setAvailableActions() {
|
||||
this.store.currentAvailableActions = {
|
||||
"order": "controller,keyboard",
|
||||
"1,Enter": "Start",
|
||||
"2,Space": "Details",
|
||||
"Power,Escape": "See options"
|
||||
};
|
||||
}
|
||||
}
|
||||
94
frontend/src/inputs/key-contexts/game-preview-key-context.ts
Normal file
94
frontend/src/inputs/key-contexts/game-preview-key-context.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { KeyContext } from "./key-context";
|
||||
import { InputManager } from "../input-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();
|
||||
InputManager.switchContext("carousel");
|
||||
}
|
||||
|
||||
setAvailableActions() {
|
||||
this.store.currentAvailableActions = {
|
||||
"order": "controller,keyboard",
|
||||
"1,Enter": "Click",
|
||||
"Power,Escape": "Back"
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { ControllerState } from "../../models/controller-state";
|
||||
import { useAppStore } from "../../stores/app-store";
|
||||
|
||||
export abstract class KeyContext {
|
||||
@ -6,6 +7,9 @@ export abstract class KeyContext {
|
||||
protected store = useAppStore();
|
||||
|
||||
public handleKey(event: KeyboardEvent): void {
|
||||
this.store.currentInputDevice = "keyboard";
|
||||
event.preventDefault();
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowRight':
|
||||
this.onKeyRight();
|
||||
@ -25,11 +29,49 @@ export abstract class KeyContext {
|
||||
case 'Enter':
|
||||
this.onEnter();
|
||||
break;
|
||||
case ' ':
|
||||
this.onSpace();
|
||||
break;
|
||||
case 'r':
|
||||
console.log("Loading games");
|
||||
this.store.loadGames().then();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public handleState(state: ControllerState) {
|
||||
this.store.currentInputDevice = "controller";
|
||||
|
||||
if (state.joystick.x === 0) {
|
||||
this.onKeyLeft()
|
||||
}
|
||||
else if (state.joystick.x === 255) {
|
||||
this.onKeyRight()
|
||||
}
|
||||
|
||||
if (state.joystick.y === 0) {
|
||||
this.onKeyUp()
|
||||
}
|
||||
else if (state.joystick.y === 255) {
|
||||
this.onKeyDown()
|
||||
}
|
||||
|
||||
if ((state.buttons & 0x04) !== 0) {
|
||||
this.onEnter()
|
||||
}
|
||||
|
||||
if ((state.buttons & 0x08) !== 0) {
|
||||
this.onSpace()
|
||||
}
|
||||
|
||||
// TODO should be 0x01 when the power button will work
|
||||
if ((state.buttons & 0x01) !== 0) {
|
||||
this.onEscape()
|
||||
}
|
||||
}
|
||||
|
||||
protected onKeyRight(): void {
|
||||
console.log('onKeyRight');
|
||||
}
|
||||
@ -53,4 +95,10 @@ export abstract class KeyContext {
|
||||
protected onEnter(): void {
|
||||
console.log('onEnter');
|
||||
}
|
||||
|
||||
protected onSpace(): void {
|
||||
console.log('onSpace');
|
||||
}
|
||||
|
||||
public setAvailableActions() {}
|
||||
}
|
||||
14
frontend/src/inputs/key-contexts/options-key-context.ts
Normal file
14
frontend/src/inputs/key-contexts/options-key-context.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { InputManager } from "../input-manager";
|
||||
import { ModalKeyContext } from "./modal-key-context";
|
||||
|
||||
export class OptionsKeyContext extends ModalKeyContext {
|
||||
public name: string = "OptionsKeyContext";
|
||||
|
||||
protected override onEscape(): void {
|
||||
super.onEscape();
|
||||
this.store.optionsOpen = false;
|
||||
this.store.qrLink = "";
|
||||
InputManager.switchContext('carousel');
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { KeyContext } from "./key-context";
|
||||
import { KeyboardManager } from "../keyboard-manager";
|
||||
import { InputManager } from "../input-manager";
|
||||
|
||||
export class SidebarKeyContext extends KeyContext {
|
||||
readonly name: string = "SidebarContext";
|
||||
@ -17,13 +17,13 @@ export class SidebarKeyContext extends KeyContext {
|
||||
protected onKeyRight() {
|
||||
super.onKeyRight();
|
||||
this.store.moveGameRight();
|
||||
KeyboardManager.switchContext("carousel")
|
||||
InputManager.switchContext("carousel")
|
||||
}
|
||||
|
||||
protected onEnter() {
|
||||
super.onEnter();
|
||||
this.store.moveGameRight();
|
||||
KeyboardManager.switchContext("carousel")
|
||||
InputManager.switchContext("carousel")
|
||||
}
|
||||
|
||||
protected onEscape() {
|
||||
@ -2,7 +2,10 @@ import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css';
|
||||
import { createPinia } from "pinia";
|
||||
import { InputManager } from "./inputs/input-manager";
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.mount('#app');
|
||||
InputManager.bind();
|
||||
InputManager.switchContext("carousel")
|
||||
|
||||
8
frontend/src/models/controller-state.ts
Normal file
8
frontend/src/models/controller-state.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ControllerState {
|
||||
id?: number,
|
||||
joystick: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
buttons: number
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
export interface Game {
|
||||
id: number | string;
|
||||
title: string;
|
||||
description?: string;
|
||||
playerCount: "1" | "2" | "1-2";
|
||||
teamMember?: TeamMember[];
|
||||
languages?: string[];
|
||||
tags?: string[];
|
||||
media?: string[];
|
||||
thumbnail?: string;
|
||||
links?: object;
|
||||
metadata?: object;
|
||||
executablePath: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
id: number | string;
|
||||
name: string;
|
||||
role?: string;
|
||||
picture?: string;
|
||||
socials?: object;
|
||||
}
|
||||
@ -1,53 +1,129 @@
|
||||
import { Game } from "../models/game";
|
||||
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'
|
||||
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",
|
||||
publicRepositoryLink: "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: '/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'
|
||||
thumbnailPath: "https://img.itch.zone/aW1hZ2UvMTkwMzc5MS8xMTgzNzY0Ny5wbmc=/original/r7iVIj.png",
|
||||
itchLink: "https://craftelia.itch.io/gamelab2023-blood-god",
|
||||
mediaPaths: [
|
||||
"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"
|
||||
],
|
||||
thumbnail: '/assets/ghost-bakery-thumb.jpg',
|
||||
links: {
|
||||
Itch: 'https://phantomforks.itch.io/ghostbakery'
|
||||
logoPath: "https://img.itch.zone/aW1nLzExODI4OTQyLnBuZw==/original/0MBnt3.png",
|
||||
backgroundImagePath: "https://img.itch.zone/aW1nLzExODIyNzg4LnBuZw==/original/QVGL4L.png",
|
||||
collections: "Ubi Gamelab",
|
||||
colorScheme: {
|
||||
primary: "#66258a",
|
||||
secondary: "#80f071",
|
||||
ternary: "#ffffff"
|
||||
},
|
||||
executablePath: "."
|
||||
}
|
||||
}),
|
||||
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",
|
||||
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"
|
||||
}),
|
||||
],
|
||||
thumbnailPath: "https://img.itch.zone/aW1nLzIwNzAwNDg1LnBuZw==/original/qGDxOj.png",
|
||||
itchLink: "https://jeanne444.itch.io/froggin-around",
|
||||
mediaPaths: [
|
||||
"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"
|
||||
],
|
||||
logoPath: "https://img.itch.zone/aW1hZ2UvMzQ2ODYzOS8yMDg2MDc1OC5wbmc=/original/tgccKa.png",
|
||||
collections: "Ubi Gamelab",
|
||||
colorScheme: {
|
||||
primary: "#cf2d30",
|
||||
secondary: "#b3cf43",
|
||||
ternary: "#ffffff"
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export async function fetchGames(): Promise<Game[]> {
|
||||
const source = localStorage.getItem('dataSource') || 'local';
|
||||
if (source === 'local') return localGames;
|
||||
|
||||
// TODO games should be loaded from and started from the wails/go code
|
||||
try {
|
||||
const res = await fetch('https://your-api-server.com/api/games');
|
||||
if (!res.ok) throw new Error('Failed to fetch games');
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
console.error('API fetch failed, falling back to local:', e);
|
||||
return localGames;
|
||||
const games = await LoadGames();
|
||||
for (const game of games) {
|
||||
console.log(game)
|
||||
}
|
||||
return [...games.map(convertToNewFormat), ...localGames];
|
||||
}
|
||||
|
||||
function convertToNewFormat(metadata: Metadata): Game {
|
||||
return new Game({
|
||||
id: metadata.Id,
|
||||
title: metadata.Game,
|
||||
description: metadata.Description,
|
||||
collections: metadata.Collection,
|
||||
thumbnailPath: metadata.ThumbnailPath,
|
||||
mediaPaths: [metadata.ImagePath],
|
||||
genres: metadata.Genres,
|
||||
executable: metadata.Files,
|
||||
release: metadata.Release,
|
||||
modification: metadata.Modification,
|
||||
version: metadata.Version,
|
||||
players: metadata.Players,
|
||||
publicRepositoryLink: metadata.PublicRepositoryLink,
|
||||
developers: metadata.Developers.split(",").map((dev) => new Developer({
|
||||
name: dev
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
35
frontend/src/services/image-service.ts
Normal file
35
frontend/src/services/image-service.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { LoadImage } from "../../wailsjs/go/main/App";
|
||||
|
||||
export class ImageService {
|
||||
|
||||
static Dictionary: {[key: string]: string} = {}
|
||||
|
||||
public static async getImage(gameId: string, src: string): Promise<string> {
|
||||
|
||||
if (!src || src.startsWith("http")) {
|
||||
return src;
|
||||
}
|
||||
|
||||
const id = gameId + "\\" + src;
|
||||
console.log(gameId, src, id)
|
||||
if (this.Dictionary[id])
|
||||
return this.Dictionary[id]
|
||||
|
||||
const fileBlob = await LoadImage(gameId, src);
|
||||
console.log(fileBlob)
|
||||
const byteCharacters = atob(fileBlob.Data as any as string);
|
||||
const byteNumbers = Array.from(byteCharacters).map(c => c.charCodeAt(0));
|
||||
console.log(byteCharacters, byteNumbers)
|
||||
|
||||
const bytes = new Uint8Array(byteNumbers);
|
||||
console.log(bytes)
|
||||
const blob = new Blob([bytes], {type: fileBlob.MimeType });
|
||||
console.log(blob)
|
||||
const url = URL.createObjectURL(blob);
|
||||
console.log(url)
|
||||
this.Dictionary[id] = url
|
||||
console.log(this.Dictionary)
|
||||
return url;
|
||||
}
|
||||
|
||||
}
|
||||
6
frontend/src/services/logger-service.ts
Normal file
6
frontend/src/services/logger-service.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Log } from "../../wailsjs/go/main/App";
|
||||
|
||||
export function log(msg: any): void {
|
||||
console.log(msg)
|
||||
Log("[Frontend] " + msg.toString()).then();
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { Game } from "../models/game";
|
||||
import {models} from "../../wailsjs/go/models";
|
||||
import Game = models.Game;
|
||||
import { StartGame } from "../../wailsjs/go/main/App";
|
||||
import { fetchGames } from '../services/game-service';
|
||||
import { InputManager } from '../inputs/input-manager';
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
@ -9,14 +13,30 @@ export const useAppStore = defineStore('app', {
|
||||
transitionDirection: 'down' as 'up' | 'down',
|
||||
selectedGame: null as Game | null,
|
||||
selectedGameIndex: 0,
|
||||
qrLink: '' as string
|
||||
qrLink: '' as string,
|
||||
optionsOpen: false as boolean,
|
||||
gameIsStarting: false as boolean,
|
||||
showSidebar: false as boolean,
|
||||
currentInputDevice: 'controller' as 'controller' | 'keyboard',
|
||||
currentAvailableActions: {} as object
|
||||
}),
|
||||
getters: {
|
||||
filteredGames(state): Game[] {
|
||||
return state.games.filter(game => game.tags.includes(state.selectedTag ?? ''));
|
||||
if (state.showSidebar) {
|
||||
return state.games.filter(game => game.genres.split(",").includes(state.selectedTag ?? ''));
|
||||
}
|
||||
return state.games;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadGames() {
|
||||
this.games = await fetchGames();
|
||||
console.log(this.games);
|
||||
this.tags = [...new Set(this.games.flatMap(game => game.genres.split(",")))];
|
||||
if (this.tags.length > 0) {
|
||||
this.selectTag(this.tags[0])
|
||||
}
|
||||
},
|
||||
moveGameRight() {
|
||||
this.selectGame(this.selectedGameIndex + 1);
|
||||
},
|
||||
@ -35,18 +55,33 @@ export const useAppStore = defineStore('app', {
|
||||
},
|
||||
selectGame(index: number) {
|
||||
const games = this.filteredGames;
|
||||
if (index >= 0 && index < games.length) {
|
||||
console.log(index)
|
||||
index = mod(index, games.length);
|
||||
console.log(index)
|
||||
this.selectedGameIndex = index;
|
||||
this.selectedGame = games[index];
|
||||
}
|
||||
this.selectedGame = games[index]
|
||||
},
|
||||
selectTag(tag: string) {
|
||||
this.selectedTag = tag;
|
||||
this.selectedGameIndex = 0;
|
||||
this.selectedGame = this.filteredGames[0] ?? null;
|
||||
this.selectGame(this.selectedGameIndex);
|
||||
},
|
||||
showQr(link: string) {
|
||||
this.qrLink = link;
|
||||
InputManager.switchContext("options");
|
||||
},
|
||||
async startSelectedGame() {
|
||||
if (this.selectedGame && !this.gameIsStarting) {
|
||||
this.gameIsStarting = true;
|
||||
await StartGame(this.selectedGame.id);
|
||||
this.gameIsStarting = false;
|
||||
}
|
||||
else {
|
||||
console.log("No game selected")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function mod(n, m) {
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { KeyContext } from "./key-context";
|
||||
import { KeyboardManager } from "../keyboard-manager";
|
||||
|
||||
export class CarouselKeyContext extends KeyContext {
|
||||
readonly name: string = "CarouselContext";
|
||||
|
||||
protected onKeyUp() {
|
||||
super.onKeyUp();
|
||||
this.store.moveTagUp();
|
||||
}
|
||||
|
||||
protected onKeyDown() {
|
||||
super.onKeyDown();
|
||||
this.store.moveTagDown();
|
||||
}
|
||||
|
||||
protected onKeyRight() {
|
||||
super.onKeyRight();
|
||||
this.store.moveGameRight();
|
||||
}
|
||||
|
||||
protected onKeyLeft() {
|
||||
super.onKeyLeft();
|
||||
if (this.store.selectedGameIndex === 0) {
|
||||
KeyboardManager.switchContext("sidebar");
|
||||
}
|
||||
else {
|
||||
this.store.moveGameLeft();
|
||||
}
|
||||
}
|
||||
|
||||
protected onEscape() {
|
||||
super.onEscape();
|
||||
}
|
||||
|
||||
protected onEnter() {
|
||||
super.onEnter();
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { KeyContext } from "./key-context";
|
||||
|
||||
export class GamePreviewKeyContext extends KeyContext {
|
||||
readonly name: string = "GamePreviewKeyContext";
|
||||
|
||||
protected onKeyUp() {
|
||||
super.onKeyUp();
|
||||
}
|
||||
|
||||
protected onKeyDown() {
|
||||
super.onKeyDown();
|
||||
}
|
||||
|
||||
protected onKeyRight() {
|
||||
super.onKeyRight();
|
||||
}
|
||||
|
||||
protected onEnter() {
|
||||
super.onEnter();
|
||||
}
|
||||
|
||||
protected onEscape() {
|
||||
super.onEscape();
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { CarouselKeyContext } from "./key-contexts/carousel-key-context";
|
||||
import { KeyContext } from "./key-contexts/key-context";
|
||||
import { SidebarKeyContext } from "./key-contexts/sidebar-key-context";
|
||||
|
||||
export class KeyboardManager {
|
||||
private static current?: KeyContext;
|
||||
|
||||
static switchContext(name: 'sidebar' | 'carousel') {
|
||||
console.log("Switching context to " + name);
|
||||
if (name === 'sidebar') this.current = new SidebarKeyContext();
|
||||
else this.current = new CarouselKeyContext();
|
||||
}
|
||||
|
||||
static handle(event: KeyboardEvent) {
|
||||
this.current?.handleKey(event);
|
||||
}
|
||||
}
|
||||
4
frontend/src/utils/string-utils.ts
Normal file
4
frontend/src/utils/string-utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export function toUpperCamelCase(str: string): string {
|
||||
const [firstChar, ...otherChars] = str;
|
||||
return firstChar.toUpperCase() + otherChars.join('').toLowerCase();
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
import { KeyboardManager } from "./keyboard-manager";
|
||||
|
||||
export function useKeyboardNavigation(): void {
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', KeyboardManager.handle.bind(KeyboardManager));
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', KeyboardManager.handle);
|
||||
});
|
||||
}
|
||||
11
frontend/wailsjs/go/main/App.d.ts
vendored
11
frontend/wailsjs/go/main/App.d.ts
vendored
@ -1,9 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {provider} from '../models';
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
export function LoadGames():Promise<Array<models.Metadata>>;
|
||||
|
||||
export function LoadGames():Promise<Array<provider.Game>>;
|
||||
export function LoadGamesNewModel():Promise<Array<models.Game>>;
|
||||
|
||||
export function SelectGame(arg1:string):Promise<void>;
|
||||
export function LoadImage(arg1:string,arg2:string):Promise<provider.FileBlob>;
|
||||
|
||||
export function Log(arg1:string):Promise<void>;
|
||||
|
||||
export function StartGame(arg1:string):Promise<void>;
|
||||
|
||||
@ -2,14 +2,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function LoadGames() {
|
||||
return window['go']['main']['App']['LoadGames']();
|
||||
}
|
||||
|
||||
export function SelectGame(arg1) {
|
||||
return window['go']['main']['App']['SelectGame'](arg1);
|
||||
export function LoadGamesNewModel() {
|
||||
return window['go']['main']['App']['LoadGamesNewModel']();
|
||||
}
|
||||
|
||||
export function LoadImage(arg1, arg2) {
|
||||
return window['go']['main']['App']['LoadImage'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function Log(arg1) {
|
||||
return window['go']['main']['App']['Log'](arg1);
|
||||
}
|
||||
|
||||
export function StartGame(arg1) {
|
||||
return window['go']['main']['App']['StartGame'](arg1);
|
||||
}
|
||||
|
||||
@ -1,11 +1,61 @@
|
||||
export namespace provider {
|
||||
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 {
|
||||
Title: string;
|
||||
Developper: string;
|
||||
Year: number;
|
||||
Cartridge: string;
|
||||
Description: string;
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
players: string;
|
||||
release: string;
|
||||
modification: string;
|
||||
publicRepositoryLink: string;
|
||||
itchLink: string;
|
||||
genres: string;
|
||||
collections: string;
|
||||
executable: string;
|
||||
thumbnailPath: string;
|
||||
logoPath: string;
|
||||
backgroundImagePath: string;
|
||||
mediaPaths: string[];
|
||||
audioPath: string;
|
||||
developers: Developer[];
|
||||
colorScheme: ColorScheme;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Game(source);
|
||||
@ -13,11 +63,104 @@ export namespace provider {
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Title = source["Title"];
|
||||
this.Developper = source["Developper"];
|
||||
this.Year = source["Year"];
|
||||
this.Cartridge = source["Cartridge"];
|
||||
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.publicRepositoryLink = source["publicRepositoryLink"];
|
||||
this.itchLink = source["itchLink"];
|
||||
this.genres = source["genres"];
|
||||
this.collections = source["collections"];
|
||||
this.executable = source["executable"];
|
||||
this.thumbnailPath = source["thumbnailPath"];
|
||||
this.logoPath = source["logoPath"];
|
||||
this.backgroundImagePath = source["backgroundImagePath"];
|
||||
this.mediaPaths = source["mediaPaths"];
|
||||
this.audioPath = source["audioPath"];
|
||||
this.developers = this.convertValues(source["developers"], Developer);
|
||||
this.colorScheme = this.convertValues(source["colorScheme"], 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;
|
||||
Game: string;
|
||||
Version: string;
|
||||
Description: string;
|
||||
Players: string;
|
||||
ThumbnailPath: string;
|
||||
ImagePath: string;
|
||||
Release: string;
|
||||
Modification: string;
|
||||
Files: string;
|
||||
PublicRepositoryLink: string;
|
||||
Genres: string;
|
||||
Developers: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Metadata(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Collection = source["Collection"];
|
||||
this.Launch = source["Launch"];
|
||||
this.Id = source["Id"];
|
||||
this.Game = source["Game"];
|
||||
this.Version = source["Version"];
|
||||
this.Description = source["Description"];
|
||||
this.Players = source["Players"];
|
||||
this.ThumbnailPath = source["ThumbnailPath"];
|
||||
this.ImagePath = source["ImagePath"];
|
||||
this.Release = source["Release"];
|
||||
this.Modification = source["Modification"];
|
||||
this.Files = source["Files"];
|
||||
this.PublicRepositoryLink = source["PublicRepositoryLink"];
|
||||
this.Genres = source["Genres"];
|
||||
this.Developers = source["Developers"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace provider {
|
||||
|
||||
export class FileBlob {
|
||||
Name: string;
|
||||
MimeType: string;
|
||||
Data: number[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FileBlob(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Name = source["Name"];
|
||||
this.MimeType = source["MimeType"];
|
||||
this.Data = source["Data"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
go.mod
41
go.mod
@ -1,42 +1,45 @@
|
||||
module conjure-os
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.23.5
|
||||
|
||||
require (
|
||||
github.com/karalabe/hid v1.0.0
|
||||
github.com/wailsapp/wails/v2 v2.9.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.16 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.9.2 => /Users/tristan/go/pkg/mod
|
||||
|
||||
109
go.sum
109
go.sum
@ -1,95 +1,96 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/karalabe/hid v1.0.0 h1:+/CIMNXhSU/zIJgnIvBD2nKHxS/bnRHhhs9xBryLpPo=
|
||||
github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
|
||||
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
|
||||
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
|
||||
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
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.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
|
||||
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
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=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
48
lib/config/config.go
Normal file
48
lib/config/config.go
Normal file
@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const conjureDirectoryName = "conjure"
|
||||
const conjureOsDirectoryName = "conjure-os"
|
||||
const conjureOsGameDirectoryName = "conj"
|
||||
const conjureOsLogsDirectoryName = "logs"
|
||||
const conjureOsConfigFileName = "config.json"
|
||||
|
||||
type AppConfig struct {
|
||||
}
|
||||
|
||||
func GetDefaultConjureOsDirectory() string {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
// I want the data to be store in LocalAppData not RoamingAppData
|
||||
case "windows":
|
||||
return filepath.Join(cacheDir, conjureDirectoryName, conjureOsDirectoryName)
|
||||
case "darwin":
|
||||
case "linux":
|
||||
return filepath.Join(configDir, conjureDirectoryName, conjureOsDirectoryName)
|
||||
default:
|
||||
panic("Unsupported platform")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetDefaultConjureGamesDirectory() string {
|
||||
return filepath.Join(GetDefaultConjureOsDirectory(), conjureOsGameDirectoryName)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
package inputs
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/karalabe/hid"
|
||||
)
|
||||
|
||||
type JoystickEvent struct {
|
||||
@ -20,30 +19,136 @@ const (
|
||||
JS_EVENT_INIT = 0x80 // Initial state of device
|
||||
)
|
||||
|
||||
func Start() {
|
||||
// Open the joystick device file
|
||||
file, err := os.Open("/dev/input/js0")
|
||||
type Controller struct {
|
||||
Device *hid.Device
|
||||
}
|
||||
|
||||
func (c *Controller) ReadState(buf []byte) (*ControllerState, error) {
|
||||
_, err := c.Device.Read(buf)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error opening joystick:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x := buf[0] // Horizontal axis (0–255)
|
||||
y := buf[1] // Vertical axis (0–255)
|
||||
buttons := buf[6] // Buttons as bitfield
|
||||
state := ControllerState{
|
||||
Joystick: Vec2B{x, y},
|
||||
Buttons: buttons,
|
||||
}
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
type Vec2B struct {
|
||||
X byte `json:"x"`
|
||||
Y byte `json:"y"`
|
||||
}
|
||||
|
||||
type ControllerState struct {
|
||||
Id int `json:"id"`
|
||||
Joystick Vec2B `json:"joystick"`
|
||||
Buttons byte `json:"buttons"`
|
||||
}
|
||||
|
||||
type ConjureControllerButton int
|
||||
|
||||
const (
|
||||
ButtonPower ConjureControllerButton = iota //0
|
||||
ButtonStart
|
||||
|
||||
Button1
|
||||
Button2
|
||||
Button3
|
||||
|
||||
ButtonA
|
||||
ButtonB
|
||||
ButtonC // 7
|
||||
)
|
||||
|
||||
var ConjureControllerButtons = []ConjureControllerButton{
|
||||
ButtonStart,
|
||||
ButtonPower,
|
||||
|
||||
Button1,
|
||||
Button2,
|
||||
Button3,
|
||||
|
||||
ButtonA,
|
||||
ButtonB,
|
||||
ButtonC,
|
||||
}
|
||||
|
||||
func (s ConjureControllerButton) String() string {
|
||||
switch s {
|
||||
case ButtonA:
|
||||
return "ButtonA"
|
||||
case ButtonB:
|
||||
return "ButtonB"
|
||||
case ButtonC:
|
||||
return "ButtonC"
|
||||
case Button1:
|
||||
return "Button1"
|
||||
case Button2:
|
||||
return "Button2"
|
||||
case Button3:
|
||||
return "Button3"
|
||||
case ButtonStart:
|
||||
return "ButtonStart"
|
||||
case ButtonPower:
|
||||
return "ButtonPower"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func Start(onStateChange func(ControllerState)) {
|
||||
fmt.Println("Opening devices")
|
||||
const vendorID = 0x0079
|
||||
const productID = 0x0006
|
||||
|
||||
devices := hid.Enumerate(vendorID, productID)
|
||||
if len(devices) == 0 {
|
||||
fmt.Printf("Device with VID:PID %04X:%04X not found\n", vendorID, productID)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Continuously read joystick events
|
||||
for i, d := range devices {
|
||||
fmt.Printf("Found device %d: %s - VID:%04X PID:%04X\n", i, d.Product, d.VendorID, d.ProductID)
|
||||
|
||||
go func(i int, d hid.DeviceInfo) {
|
||||
device, err := d.Open()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open device %d: %v\n", i, err)
|
||||
return
|
||||
}
|
||||
defer device.Close()
|
||||
|
||||
controller := Controller{Device: device}
|
||||
buf := make([]byte, 32)
|
||||
|
||||
fmt.Printf("Reading data from device %d... Press Ctrl+C to exit\n", i)
|
||||
for {
|
||||
var e JoystickEvent
|
||||
err := binary.Read(file, binary.LittleEndian, &e)
|
||||
state, err := controller.ReadState(buf)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading joystick event:", err)
|
||||
fmt.Printf("Read error on device %d: %v\n", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
handleJoystickEvent(e)
|
||||
state.Id = i
|
||||
|
||||
// Sleep to avoid flooding output
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
// TODO Samuel please fix help me
|
||||
if (state.Buttons != 0 || state.Joystick.X != 127 || state.Joystick.Y != 127) && !(state.Id == 1 && state.Buttons == 128 && state.Joystick.X == 127 && state.Joystick.Y == 127) {
|
||||
// fmt.Printf("Id: %d - %d buttons ", state.Id, state.Buttons)
|
||||
onStateChange(*state)
|
||||
}
|
||||
}
|
||||
}(i, d)
|
||||
}
|
||||
|
||||
// Prevent the function from exiting
|
||||
select {}
|
||||
}
|
||||
|
||||
// handleJoystickEvent processes joystick events.
|
||||
|
||||
55
lib/models/game.go
Normal file
55
lib/models/game.go
Normal file
@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
type Metadata struct {
|
||||
Collection string
|
||||
Launch string
|
||||
Id string
|
||||
Game string
|
||||
Version string
|
||||
Description string
|
||||
Players string
|
||||
ThumbnailPath string `yaml:"thumbnailPath"`
|
||||
ImagePath string `yaml:"imagePath"`
|
||||
Release string
|
||||
Modification string
|
||||
Files string
|
||||
PublicRepositoryLink string `yaml:"publicRepositoryLink"`
|
||||
Genres string
|
||||
Developers 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:"publicRepositoryLink"`
|
||||
ItchLink string `json:"itchLink"`
|
||||
Genres string `json:"genres"`
|
||||
Collections string `json:"collections"`
|
||||
Executable string `json:"executable"`
|
||||
ThumbnailPath string `json:"thumbnailPath"`
|
||||
LogoPath string `json:"logoPath"`
|
||||
BackgroundImagePath string `json:"backgroundImagePath"`
|
||||
MediaPaths []string `json:"mediaPaths"`
|
||||
AudioPath string `json:"audioPath"`
|
||||
Developers []Developer `json:"developers"`
|
||||
ColorScheme ColorScheme `json:"colorScheme"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
53
lib/provider/file-provider.go
Normal file
53
lib/provider/file-provider.go
Normal file
@ -0,0 +1,53 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"conjure-os/lib/config"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func LoadImage(gameId string, imageSrc string) *FileBlob {
|
||||
imagePath := filepath.Join(config.GetDefaultConjureGamesDirectory(), gameId, imageSrc)
|
||||
blob, err := GetFileBlob(imagePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return blob
|
||||
}
|
||||
|
||||
type FileBlob struct {
|
||||
Name string
|
||||
MimeType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func GetFileBlob(path string) (*FileBlob, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to guess MIME type from content
|
||||
mimeType := http.DetectContentType(data)
|
||||
|
||||
// Fallback to extension-based detection
|
||||
if mimeType == "application/octet-stream" {
|
||||
ext := filepath.Ext(path)
|
||||
if ext != "" {
|
||||
if t := mime.TypeByExtension(ext); t != "" {
|
||||
mimeType = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
name := filepath.Base(path)
|
||||
|
||||
return &FileBlob{
|
||||
Name: name,
|
||||
MimeType: mimeType,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
@ -1,34 +1,28 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"conjure-os/lib/config"
|
||||
"conjure-os/lib/models"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
Title string
|
||||
Developper string
|
||||
Year int
|
||||
Cartridge string
|
||||
Description string
|
||||
}
|
||||
|
||||
var token string
|
||||
|
||||
const game_path = "/Users/tristan/Downloads/conjure-os"
|
||||
|
||||
func Update() {
|
||||
requestURL := fmt.Sprintf("http://localhost:%d", 8080)
|
||||
login(requestURL)
|
||||
allGames(requestURL)
|
||||
//requestURL := fmt.Sprintf("http://localhost:%d", 8080)
|
||||
//login(requestURL)
|
||||
//allGames(requestURL)
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
@ -63,55 +57,205 @@ func allGames(requestURL string) {
|
||||
fmt.Printf("client: status code: %d\n", res.StatusCode)
|
||||
}
|
||||
|
||||
func ObtainConjureGameInfo() []Game {
|
||||
entries, err := os.ReadDir(game_path)
|
||||
func GetOrSetEnvKey(key string, defaultValue string) string {
|
||||
value, exist := os.LookupEnv(key)
|
||||
if exist {
|
||||
return value
|
||||
}
|
||||
err := os.Setenv(key, defaultValue)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func extractZipToSiblingFolder(zipPath string) error {
|
||||
// Determine destination folder name (same name as zip file, without .zip)
|
||||
zipBase := strings.TrimSuffix(filepath.Base(zipPath), ".conj")
|
||||
zipBase = strings.TrimSuffix(filepath.Base(zipBase), ".zip")
|
||||
destDir := filepath.Join(filepath.Dir(zipPath), zipBase)
|
||||
|
||||
// Delete destination folder if it exists
|
||||
if _, err := os.Stat(destDir); err == nil {
|
||||
err = os.RemoveAll(destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing folder: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Open the zip archive
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Create the destination directory
|
||||
err = os.MkdirAll(destDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
destPath := filepath.Join(destDir, f.Name)
|
||||
|
||||
// ZipSlip protection
|
||||
if !strings.HasPrefix(destPath, filepath.Clean(destDir)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("illegal file path: %s", destPath)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(destPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure parent directories exist
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and copy the file
|
||||
srcFile, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZipsInFolder(folder string) error {
|
||||
entries, err := os.ReadDir(folder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading directory failed: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(entry.Name()), ".zip") {
|
||||
zipPath := filepath.Join(folder, entry.Name())
|
||||
fmt.Println("Extracting:", zipPath)
|
||||
if err := extractZipToSiblingFolder(zipPath); err != nil {
|
||||
return fmt.Errorf("failed to extract %s: %w", zipPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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))
|
||||
err = extractZipToSiblingFolder(gamePath)
|
||||
check(err)
|
||||
}
|
||||
gamePath := filepath.Join(config.GetDefaultConjureGamesDirectory(), game.Id)
|
||||
|
||||
_, err = os.Stat(filepath.Join(gamePath, game.Files))
|
||||
if err == nil {
|
||||
return filepath.Join(gamePath, game.Files)
|
||||
}
|
||||
|
||||
err = extractZipsInFolder(gamePath)
|
||||
check(err)
|
||||
gamePath = filepath.Join(gamePath, game.Files)
|
||||
return gamePath
|
||||
}
|
||||
|
||||
func GetConjureGameInfo() []models.Metadata {
|
||||
gamePath := config.GetDefaultConjureGamesDirectory()
|
||||
|
||||
fmt.Println("Loading games....")
|
||||
fmt.Println(gamePath)
|
||||
|
||||
entries, err := os.ReadDir(gamePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
games := []Game{}
|
||||
|
||||
var games []models.Metadata
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
newGames := readFolder(e, game_path)
|
||||
games = append(newGames, games...)
|
||||
continue
|
||||
} else if filepath.Ext(e.Name()) == ".conj" {
|
||||
conjPath := filepath.Join(gamePath, e.Name())
|
||||
conjBase := strings.TrimSuffix(conjPath, ".conj")
|
||||
|
||||
// Check if the destination folder already exists
|
||||
if _, err := os.Stat(conjBase); os.IsNotExist(err) {
|
||||
err = extractZipToSiblingFolder(conjPath)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Now read metadata from the extracted directory
|
||||
entries, err := os.ReadDir(conjBase)
|
||||
check(err)
|
||||
|
||||
fmt.Println("Contents of", conjPath)
|
||||
for _, f := range entries {
|
||||
if f.Name() == "metadata.txt" {
|
||||
rc, err := os.Open(filepath.Join(conjBase, f.Name()))
|
||||
check(err)
|
||||
defer rc.Close()
|
||||
|
||||
fmt.Println("Contents of metadata.txt:")
|
||||
metadata, err := io.ReadAll(rc)
|
||||
check(err)
|
||||
game := parseGameInfo([]byte(escapeBackslashes(string(metadata))))
|
||||
fmt.Println(game.ThumbnailPath)
|
||||
games = append(games, game)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(games) > 0 {
|
||||
fmt.Println("Found Conjure Games: " + string(len(games)))
|
||||
fmt.Println("Found Conjure Games:", len(games))
|
||||
} else {
|
||||
fmt.Println("No Conjure games Found")
|
||||
}
|
||||
return games
|
||||
}
|
||||
|
||||
func readFolder(entry fs.DirEntry, path string) []Game {
|
||||
newPath := path + "/" + entry.Name()
|
||||
entries, err := os.ReadDir(newPath)
|
||||
check(err)
|
||||
|
||||
games := []Game{}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
games = append(games, readFolder(e, newPath)...)
|
||||
} else {
|
||||
filenamesplit := strings.Split(e.Name(), ".")
|
||||
|
||||
if filenamesplit[1] == "conf" && filenamesplit[2] == "yml" {
|
||||
game := parseGameInfoFromFile(newPath + "/" + e.Name())
|
||||
games = append(games, game)
|
||||
}
|
||||
}
|
||||
}
|
||||
return games
|
||||
// Function to escape backslashes in the YAML string
|
||||
func escapeBackslashes(input string) string {
|
||||
// Replace every single backslash with double backslashes
|
||||
return strings.ReplaceAll(input, `\`, `\\`)
|
||||
}
|
||||
|
||||
func parseGameInfoFromFile(path string) Game {
|
||||
data, err := os.ReadFile(path)
|
||||
check(err)
|
||||
game := Game{}
|
||||
err = yaml.Unmarshal(data, &game)
|
||||
// Helper to print tree-like structure
|
||||
func printIndentedPath(path string) {
|
||||
parts := strings.Split(path, "/")
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Print(strings.Repeat(" ", i))
|
||||
fmt.Println(part)
|
||||
}
|
||||
}
|
||||
|
||||
func parseGameInfo(data []byte) models.Metadata {
|
||||
game := models.Metadata{}
|
||||
err := yaml.Unmarshal(data, &game)
|
||||
check(err)
|
||||
return game
|
||||
}
|
||||
|
||||
30
main.go
30
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()
|
||||
@ -20,11 +45,12 @@ func main() {
|
||||
Title: "conjure-os",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
Fullscreen: true,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
Handler: NewFileLoader(),
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
OnStartup: app.Startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user