diff --git a/.gitignore b/.gitignore index 59ad4c4..a83910e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ frontend/dist frontend/wailsjs */package-lock.json */package.json.md5 -*.exe \ No newline at end of file +*.exe +*.o +*.rc +frontend/public \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4396ea7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,63 @@ +{ + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "format": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "semaphore": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "typeinfo": "cpp", + "variant": "cpp" + } +} \ No newline at end of file diff --git a/app.go b/app.go index 64211c3..f55e03f 100644 --- a/app.go +++ b/app.go @@ -2,150 +2,78 @@ package main import ( "context" + "dmxconnect/hardware" "fmt" "io" - "log" + "time" + + "github.com/rs/zerolog/log" + "os" - "path/filepath" "strings" - - "github.com/wailsapp/wails/v2/pkg/runtime" - - "gopkg.in/yaml.v2" -) - -const ( - projectsDirectory = "projects" // The directory were are stored all the projects - avatarsDirectory = "frontend/public" // The directory were are stored all the avatars - projectExtension = ".dmxproj" // The extension of a DMX Connect project + "sync" ) // App struct type App struct { - ctx context.Context + ctx context.Context + cancelFunc context.CancelFunc + hardwareManager *hardware.HardwareManager // For managing all the hardware + wmiMutex sync.Mutex // Avoid some WMI operations at the same time + projectInfo ProjectInfo // The project information structure + projectSave string // The file name of the project } // NewApp creates a new App application struct func NewApp() *App { - return &App{} + // Create a new hadware manager + hardwareManager := hardware.NewHardwareManager() + // hardwareManager.RegisterFinder(hardware.NewMIDIFinder(5 * time.Second)) + hardwareManager.RegisterFinder(hardware.NewFTDIFinder(5 * time.Second)) + // hardwareManager.RegisterFinder(hardware.NewOS2LFinder()) + return &App{ + hardwareManager: hardwareManager, + projectSave: "", + projectInfo: ProjectInfo{ + PeripheralsInfo: make(map[string]hardware.PeripheralInfo), + }, + } } // 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) { - a.ctx = ctx +func (a *App) onStartup(ctx context.Context) { + a.ctx, a.cancelFunc = context.WithCancel(ctx) + err := a.hardwareManager.Start(a.ctx) + if err != nil { + log.Err(err).Str("file", "app").Msg("unable to start the hardware manager") + return + } } -// GetProjects gets all the projects in the projects directory -func (a *App) GetProjects() ([]ProjectInfo, error) { - projects := []ProjectInfo{} - - f, err := os.Open(projectsDirectory) - if err != nil { - log.Fatalf("Unable to open the projects directory: %v", err) - return nil, err - } - - files, err := f.Readdir(0) - if err != nil { - log.Fatalf("Unable to read the projects directory: %v", err) - return nil, err - } - - for _, fileInfo := range files { - // Open the file and get the show name - fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name())) - if err == nil { - projectObject := ProjectInfo{} - err = yaml.Unmarshal(fileData, &projectObject) - if err == nil { - // Add the SaveFile property - projectObject.ShowInfo.SaveFile = fileInfo.Name() - projects = append(projects, projectObject) - } - } - } - return projects, nil +// onReady is called when the DOM is ready +// We get the current peripherals connected +func (a *App) onReady(ctx context.Context) { + // log.Debug().Str("file", "peripherals").Msg("getting peripherals...") + // err := a.hardwareManager.Scan() + // if err != nil { + // log.Err(err).Str("file", "app").Msg("unable to get the peripherals") + // } + return } -// GetProjectInfo returns the information of the saved project -func (a *App) GetProjectInfo(projectFile string) (ShowInfo, error) { - projectPath := filepath.Join(projectsDirectory, projectFile) - content, err := os.ReadFile(projectPath) +// onShutdown is called when the app is closing +// We stop all the pending processes +func (a *App) onShutdown(ctx context.Context) { + // Close the application properly + log.Trace().Str("file", "app").Msg("app is closing") + // Explicitly close the context + a.cancelFunc() + err := a.hardwareManager.Stop() if err != nil { - log.Fatalf("Unable to read the project file: %v", err) - return ShowInfo{}, err + log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager") } - - projectInfo := ProjectInfo{} - - err = yaml.Unmarshal(content, &projectInfo) - if err != nil { - log.Fatalf("Unable to get the project information: %v", err) - return ShowInfo{}, err - } - projectInfo.ShowInfo.SaveFile = projectFile - return projectInfo.ShowInfo, nil -} - -// ChooseAvatarPath opens a filedialog to choose the show avatar -func (a *App) ChooseAvatarPath() (string, error) { - // Open the file dialog box - filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ - Title: "Choose your show avatar", - Filters: []runtime.FileFilter{ - { - DisplayName: "Images", - Pattern: "*.png;*.jpg;*.jpeg", - }, - }, - }) - if err != nil { - return "", err - } - // Copy the avatar to the application avatars path - avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath)) - _, err = copy(filePath, avatarPath) - if err != nil { - return "", err - } - return filepath.Base(filePath), nil -} - -// SaveProject saves the project -func (a *App) SaveProject(showInfo ShowInfo) (string, error) { - log.Printf("Saving the project %s to %s", showInfo.Name, showInfo.SaveFile) - // If there is no save file, create a new one with the show name - if showInfo.SaveFile == "" { - showInfo.SaveFile = fmt.Sprintf("%s%s", formatString(showInfo.Name), projectExtension) - } - project := ProjectInfo{} - log.Printf("The number of universes: %d", showInfo.UniversesNumber) - project.ShowInfo = showInfo - data, err := yaml.Marshal(project) - if err != nil { - return "", err - } - err = os.WriteFile(filepath.Join(projectsDirectory, showInfo.SaveFile), data, os.ModePerm) - if err != nil { - return "", err - } - return showInfo.SaveFile, nil -} - -// ShowInfo defines the information of the show -type ShowInfo struct { - Name string `yaml:"name"` - Date string `yaml:"date"` - UniversesNumber int `yaml:"universesNumber"` - Avatar string `yaml:"avatar"` - Comments string `yaml:"comments"` - SaveFile string `yaml:"-"` -} - -// ProjectInfo defines all the information for a lighting project -type ProjectInfo struct { - ShowInfo ShowInfo `yaml:"show"` // Show information + return } func formatString(input string) string { diff --git a/build/appicon.png b/build/appicon.png index 63617fe..1257908 100644 Binary files a/build/appicon.png and b/build/appicon.png differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico index bfa0690..9e3ff9a 100644 Binary files a/build/windows/icon.ico and b/build/windows/icon.ico differ diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index 17e1a23..213813b 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,13 @@ + + + + + + + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 5e9fdc3..032ecde 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -9,10 +9,55 @@ import Show from './components/Show/Show.svelte'; import GeneralConsole from './components/Console/GeneralConsole.svelte'; import RoundIconButton from './components/General/RoundIconButton.svelte'; - import { generateToast, currentProject, needProjectSave } from './stores'; + import { generateToast, showInformation, needProjectSave, peripherals } from './stores'; import { SaveProject } from '../wailsjs/go/main/App.js'; - import { construct_svelte_component } from 'svelte/internal'; - import ToastNotification from './components/General/ToastNotification.svelte'; + import { construct_svelte_component } from 'svelte/internal'; + import { EventsOn } from '../wailsjs/runtime' + import { CreateProject } from "../wailsjs/go/main/App"; + import { WindowSetTitle } from "../wailsjs/runtime/runtime" + import { get } from "svelte/store" + import ToastNotification from './components/General/ToastNotification.svelte'; + + // Handle the event when a new peripheral is detected + EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){ + // When a new peripheral is detected, add it to the map and: + // - Pass the isDetected key to true + // - Set the isSaved key to the last value + let peripheralsList = get(peripherals) + let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved + peripheralInfo.isDetected = true + peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false + peripherals.update((peripherals) => { + peripherals[peripheralInfo.SerialNumber] = peripheralInfo + return {...peripherals} + }) + console.log("Hardware has been added to the system"); + generateToast('info', 'bxs-hdd', $_("peripheralArrivalToast") + ' ' + peripheralInfo.Name + '') + }) + + // Handle the event when a peripheral is removed from the system + EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){ + console.log("Hardware has been removed from the system"); + // When a peripheral is disconnected, pass its isDetected key to false + // If the isSaved key is set to false, we can completely remove the peripheral from the list + let peripheralsList = get(peripherals) + let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved + let needToDelete = (lastSavedProperty !== true) ? true : false + peripherals.update((storedPeripherals) => { + if (needToDelete){ + delete storedPeripherals[peripheralInfo.SerialNumber]; + return { ...storedPeripherals }; + } + storedPeripherals[peripheralInfo.SerialNumber].isDetected = false + return {...storedPeripherals} + }) + generateToast('warning', 'bxs-hdd', $_("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') + }) + + // Set the window title + $: { + WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : "")) + } let selectedMenu = "settings" // When the navigation menu changed, update the selected menu @@ -22,35 +67,31 @@ // Save the project function saveProject(){ - SaveProject($currentProject).then((saveFile) => { - console.log($currentProject) - $currentProject.SaveFile = saveFile + SaveProject().then((filePath) => { needProjectSave.set(false) - console.log("Project has been saved") - generateToast('info', 'bxs-save', 'The project has been saved') + console.log("Project has been saved to " + filePath) + generateToast('info', 'bxs-save', $_("projectSavedToast")) }).catch((error) => { console.error(`Unable to save the project: ${error}`) - generateToast('danger', 'bx-error', 'Unable to save the project: ' + error) + generateToast('danger', 'bx-error', $_("projectSaveErrorToast") + ' ' + error) }) } - function formatDate(date) { - const pad = (number) => number.toString().padStart(2, '0'); - const year = date.getFullYear(); - const month = pad(date.getMonth() + 1); // Les mois commencent à 0 - const day = pad(date.getDate()); - const hours = pad(date.getHours()); - const minutes = pad(date.getMinutes()); - return `${year}-${month}-${day}T${hours}:${minutes}`; - } + // Instanciate a new project + CreateProject().then((showInfo) => { + showInformation.set(showInfo) + $needProjectSave = true + }) - currentProject.set({ - Name: "My new show", - Date: formatDate(new Date()), - Avatar: "appicon.png", - UniversesNumber: 1, - Comments: "Write your comments here", - SaveFile: "", + // Handle window shortcuts + document.addEventListener('keydown', function(event) { + // Check the CTRL+S keys + if ((event.ctrlKey || event.metaKey) && event.key === 's') { + // Avoid the natural behaviour + event.preventDefault(); + // Save the current project + saveProject() + } }); @@ -58,7 +99,7 @@
{#if $needProjectSave} - + {/if}
diff --git a/frontend/src/components/General/DropdownList.svelte b/frontend/src/components/General/DropdownList.svelte index 3df0136..3ab7c07 100644 --- a/frontend/src/components/General/DropdownList.svelte +++ b/frontend/src/components/General/DropdownList.svelte @@ -10,10 +10,18 @@ export let active = false; export let style = ''; + let tooltipPosition = {top: 0, left: 0} + // Show a tooltip on mouse hover let tooltipShowing = false - function toggleTooltip(){ - tooltipShowing = !tooltipShowing + let buttonRef + function toggleTooltip(active){ + const rect = buttonRef.getBoundingClientRect(); + tooltipPosition = { + top: rect.bottom + 5, // Ajouter une marge en bas + left: rect.left, // Centrer horizontalement + }; + tooltipShowing = active } // Emit a click event when the button is clicked @@ -39,13 +47,13 @@
- - +
{#each Array.from(choices) as [key, value]} diff --git a/frontend/src/components/General/InfoButton.svelte b/frontend/src/components/General/InfoButton.svelte new file mode 100644 index 0000000..6ec350b --- /dev/null +++ b/frontend/src/components/General/InfoButton.svelte @@ -0,0 +1,63 @@ + + + +
+
{ toggleTooltip(true) }} + on:mouseleave={() => { toggleTooltip(false) }}> + +
+ {#if message} + + {/if} +
+ + + + diff --git a/frontend/src/components/General/Input.svelte b/frontend/src/components/General/Input.svelte index bf9572f..11119e7 100644 --- a/frontend/src/components/General/Input.svelte +++ b/frontend/src/components/General/Input.svelte @@ -15,21 +15,27 @@ const dispatch = createEventDispatcher(); - function handleInput(event){ - value = event.target.value - dispatch('input', value) + function handleInput(){ + dispatch('input') } + function handleBlur(event){ + dispatch('blur', event) + } + + function handleDblClick(){ + dispatch('dblclick') + }
-

{label}

+

{label}

{#if type === 'large'} -