diff --git a/app.go b/app.go index 79929f2..54ff9fb 100644 --- a/app.go +++ b/app.go @@ -124,7 +124,7 @@ func (a *App) ListCharacters() ([]CharacterInfo, error) { } list := make([]CharacterInfo, 0, len(tokens)) for _, t := range tokens { - list = append(list, CharacterInfo{CharacterID: t.CharacterID, CharacterName: t.CharacterName}) + list = append(list, CharacterInfo{CharacterID: t.CharacterID, CharacterName: t.CharacterName, WaypointEnabled: t.WaypointEnabled}) } return list, nil } @@ -139,6 +139,14 @@ func (a *App) GetCharacterLocations() ([]CharacterLocation, error) { return a.ssi.GetCharacterLocations(ctx) } +// ToggleCharacterWaypointEnabled toggles waypoint enabled status for a character +func (a *App) ToggleCharacterWaypointEnabled(characterID int64) error { + if a.ssi == nil { + return errors.New("ESI not initialised") + } + return a.ssi.ToggleCharacterWaypointEnabled(characterID) +} + // SystemRegion holds system + region names from local DB type SystemRegion struct { System string `json:"system"` diff --git a/esi_sso.go b/esi_sso.go index 6b8d743..cec4900 100644 --- a/esi_sso.go +++ b/esi_sso.go @@ -63,34 +63,36 @@ type SolarSystem struct { func (SolarSystem) TableName() string { return "mapSolarSystems" } type ESIToken struct { - ID uint `gorm:"primaryKey"` - CharacterID int64 `gorm:"index"` - CharacterName string - AccessToken string - RefreshToken string - ExpiresAt time.Time - UpdatedAt time.Time - CreatedAt time.Time + ID uint `gorm:"primaryKey"` + CharacterID int64 `gorm:"index"` + CharacterName string + AccessToken string + RefreshToken string + ExpiresAt time.Time + WaypointEnabled bool `gorm:"default:true"` + UpdatedAt time.Time + CreatedAt time.Time } type CharacterInfo struct { - CharacterID int64 `json:"character_id"` - CharacterName string `json:"character_name"` + CharacterID int64 `json:"character_id"` + CharacterName string `json:"character_name"` + WaypointEnabled bool `json:"waypoint_enabled"` } // CharacterLocation represents a character's current location type CharacterLocation struct { - CharacterID int64 `json:"character_id"` - CharacterName string `json:"character_name"` - SolarSystemID int64 `json:"solar_system_id"` - SolarSystemName string `json:"solar_system_name"` - RetrievedAt time.Time `json:"retrieved_at"` + CharacterID int64 `json:"character_id"` + CharacterName string `json:"character_name"` + SolarSystemID int64 `json:"solar_system_id"` + SolarSystemName string `json:"solar_system_name"` + RetrievedAt time.Time `json:"retrieved_at"` } type esiCharacterLocationResponse struct { - SolarSystemID int64 `json:"solar_system_id"` - StationID int64 `json:"station_id"` - StructureID int64 `json:"structure_id"` + SolarSystemID int64 `json:"solar_system_id"` + StationID int64 `json:"station_id"` + StructureID int64 `json:"structure_id"` } func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO { @@ -122,39 +124,57 @@ func (s *ESISSO) initDB() error { // resolveSystemNameByID returns the system name for an ID from the local DB, or empty if not found func (s *ESISSO) resolveSystemNameByID(id int64) string { - if s.db == nil || id == 0 { return "" } - var ss SolarSystem - if err := s.db.Select("solarSystemName").First(&ss, "solarSystemID = ?", id).Error; err != nil { return "" } - return ss.SolarSystemName + if s.db == nil || id == 0 { + return "" + } + var ss SolarSystem + if err := s.db.Select("solarSystemName").First(&ss, "solarSystemID = ?", id).Error; err != nil { + return "" + } + return ss.SolarSystemName } // GetCharacterLocations returns current locations for all stored characters func (s *ESISSO) GetCharacterLocations(ctx context.Context) ([]CharacterLocation, error) { - if s.db == nil { return nil, errors.New("db not initialised") } - var tokens []ESIToken - if err := s.db.Find(&tokens).Error; err != nil { return nil, err } - out := make([]CharacterLocation, 0, len(tokens)) - client := &http.Client{ Timeout: 5 * time.Second } - for i := range tokens { - t := &tokens[i] - tok, err := s.ensureAccessTokenFor(ctx, t) - if err != nil { continue } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/characters/"+strconv.FormatInt(t.CharacterID,10)+"/location", nil) - if err != nil { continue } - req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", "Bearer "+tok) - resp, err := client.Do(req) - if err != nil { continue } - func() { - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { return } - var lr esiCharacterLocationResponse - if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { return } - name := s.resolveSystemNameByID(lr.SolarSystemID) - out = append(out, CharacterLocation{ CharacterID: t.CharacterID, CharacterName: t.CharacterName, SolarSystemID: lr.SolarSystemID, SolarSystemName: name, RetrievedAt: time.Now() }) - }() - } - return out, nil + if s.db == nil { + return nil, errors.New("db not initialised") + } + var tokens []ESIToken + if err := s.db.Find(&tokens).Error; err != nil { + return nil, err + } + out := make([]CharacterLocation, 0, len(tokens)) + client := &http.Client{Timeout: 5 * time.Second} + for i := range tokens { + t := &tokens[i] + tok, err := s.ensureAccessTokenFor(ctx, t) + if err != nil { + continue + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/characters/"+strconv.FormatInt(t.CharacterID, 10)+"/location", nil) + if err != nil { + continue + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+tok) + resp, err := client.Do(req) + if err != nil { + continue + } + func() { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return + } + var lr esiCharacterLocationResponse + if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { + return + } + name := s.resolveSystemNameByID(lr.SolarSystemID) + out = append(out, CharacterLocation{CharacterID: t.CharacterID, CharacterName: t.CharacterName, SolarSystemID: lr.SolarSystemID, SolarSystemName: name, RetrievedAt: time.Now()}) + }() + } + return out, nil } func (s *ESISSO) saveToken() { @@ -527,6 +547,9 @@ func (s *ESISSO) PostWaypointForAll(destinationID int64, clearOthers bool, addTo } var firstErr error for i := range tokens { + if !tokens[i].WaypointEnabled { + continue + } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) tok, err := s.ensureAccessTokenFor(ctx, &tokens[i]) cancel() @@ -543,6 +566,19 @@ func (s *ESISSO) PostWaypointForAll(destinationID int64, clearOthers bool, addTo return firstErr } +// ToggleCharacterWaypointEnabled toggles the waypoint enabled status for a character +func (s *ESISSO) ToggleCharacterWaypointEnabled(characterID int64) error { + if s.db == nil { + return errors.New("db not initialised") + } + var token ESIToken + if err := s.db.Where("character_id = ?", characterID).First(&token).Error; err != nil { + return err + } + token.WaypointEnabled = !token.WaypointEnabled + return s.db.Save(&token).Error +} + func (s *ESISSO) Status() SSOStatus { s.mu.Lock() defer s.mu.Unlock() @@ -667,16 +703,24 @@ func (s *ESISSO) ResolveSystemIDsByNames(ctx context.Context, names []string) ([ // PostRouteForAll clears route and posts vias then destination last func (s *ESISSO) PostRouteForAll(destID int64, viaIDs []int64) error { - if s.db == nil { return errors.New("db not initialised") } + if s.db == nil { + return errors.New("db not initialised") + } var tokens []ESIToken - if err := s.db.Find(&tokens).Error; err != nil { return err } - // Deduplicate by CharacterID + if err := s.db.Find(&tokens).Error; err != nil { + return err + } + // Deduplicate by CharacterID and filter enabled characters uniq := make(map[int64]ESIToken, len(tokens)) for _, t := range tokens { - uniq[t.CharacterID] = t + if t.WaypointEnabled { + uniq[t.CharacterID] = t + } } uniqueTokens := make([]ESIToken, 0, len(uniq)) - for _, t := range uniq { uniqueTokens = append(uniqueTokens, t) } + for _, t := range uniq { + uniqueTokens = append(uniqueTokens, t) + } var mu sync.Mutex var firstErr error @@ -690,20 +734,53 @@ func (s *ESISSO) PostRouteForAll(destID int64, viaIDs []int64) error { tok, err := s.ensureAccessTokenFor(ctx, &t) cancel() if err != nil { - mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + return } // Post sequence for this character if len(viaIDs) > 0 { - if err := s.postWaypointWithToken(tok, viaIDs[0], true, false); err != nil { mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return } - for _, id := range viaIDs[1:] { - if err := s.postWaypointWithToken(tok, id, false, false); err != nil { mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return } + if err := s.postWaypointWithToken(tok, viaIDs[0], true, false); err != nil { + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + return + } + for _, id := range viaIDs[1:] { + if err := s.postWaypointWithToken(tok, id, false, false); err != nil { + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + return + } + } + if err := s.postWaypointWithToken(tok, destID, false, false); err != nil { + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + return } - if err := s.postWaypointWithToken(tok, destID, false, false); err != nil { mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return } } else { - if err := s.postWaypointWithToken(tok, destID, true, false); err != nil { mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return } + if err := s.postWaypointWithToken(tok, destID, true, false); err != nil { + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + return + } } }(uniqueTokens[i]) } wg.Wait() return firstErr -} \ No newline at end of file +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 82275a5..67dc9de 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,16 +2,17 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; import { toast } from '@/hooks/use-toast'; -import { StartESILogin, ESILoggedIn, ListCharacters } from 'wailsjs/go/main/App'; +import { StartESILogin, ESILoggedIn, ListCharacters, ToggleCharacterWaypointEnabled } from 'wailsjs/go/main/App'; +import { main } from 'wailsjs/go/models'; interface HeaderProps { title: string; @@ -21,16 +22,14 @@ interface HeaderProps { }>; } -interface CharacterInfo { character_id: number; character_name: string } - export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => { const navigate = useNavigate(); - const [chars, setChars] = useState([]); + const [chars, setChars] = useState([]); const refreshState = async () => { try { const list = await ListCharacters(); - setChars((list as any[]).map((c: any) => ({ character_id: c.character_id, character_name: c.character_name }))); + setChars(list); } catch { } }; @@ -55,6 +54,17 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => { } }; + const handleCharacterClick = async (character: main.CharacterInfo) => { + try { + await ToggleCharacterWaypointEnabled(character.character_id); + await refreshState(); + const newStatus = character.waypoint_enabled ? 'disabled' : 'enabled'; + toast({ title: 'Waypoint Status', description: `${character.character_name} waypoints ${newStatus}` }); + } catch (e: any) { + toast({ title: 'Toggle failed', description: String(e), variant: 'destructive' }); + } + }; + return (
{breadcrumbs.length > 0 && ( @@ -91,7 +101,16 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => { {chars.length > 0 && (
{chars.map((c) => ( - + handleCharacterClick(c)} + className={`px-2 py-1 rounded-full text-xs whitespace-nowrap cursor-pointer transition-colors ${ + c.waypoint_enabled + ? 'bg-purple-500/20 text-purple-200 border border-purple-400/40 hover:bg-purple-500/30' + : 'bg-gray-500/20 text-gray-400 border border-gray-400/40 hover:bg-gray-500/30' + }`} + title={`Click to ${c.waypoint_enabled ? 'disable' : 'enable'} waypoints for ${c.character_name}`} + > {c.character_name} ))} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 27735f7..c01e6e3 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -21,3 +21,5 @@ export function PostRouteForAllByNames(arg1:string,arg2:Array):Promise; export function StartESILogin():Promise; + +export function ToggleCharacterWaypointEnabled(arg1:number):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index fa94113..1b8fcc9 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -41,3 +41,7 @@ export function SetDestinationForAll(arg1, arg2, arg3) { export function StartESILogin() { return window['go']['main']['App']['StartESILogin'](); } + +export function ToggleCharacterWaypointEnabled(arg1) { + return window['go']['main']['App']['ToggleCharacterWaypointEnabled'](arg1); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index b951b4e..f936fa6 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -3,6 +3,7 @@ export namespace main { export class CharacterInfo { character_id: number; character_name: string; + waypoint_enabled: boolean; static createFrom(source: any = {}) { return new CharacterInfo(source); @@ -12,6 +13,7 @@ export namespace main { if ('string' === typeof source) source = JSON.parse(source); this.character_id = source["character_id"]; this.character_name = source["character_name"]; + this.waypoint_enabled = source["waypoint_enabled"]; } } export class CharacterLocation {