Enable toggling waypoint sending characters

This commit is contained in:
2025-08-28 15:33:05 +02:00
parent 2d6af8bfa9
commit 81713d09fd
6 changed files with 185 additions and 67 deletions

10
app.go
View File

@@ -124,7 +124,7 @@ func (a *App) ListCharacters() ([]CharacterInfo, error) {
} }
list := make([]CharacterInfo, 0, len(tokens)) list := make([]CharacterInfo, 0, len(tokens))
for _, t := range 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 return list, nil
} }
@@ -139,6 +139,14 @@ func (a *App) GetCharacterLocations() ([]CharacterLocation, error) {
return a.ssi.GetCharacterLocations(ctx) 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 // SystemRegion holds system + region names from local DB
type SystemRegion struct { type SystemRegion struct {
System string `json:"system"` System string `json:"system"`

View File

@@ -69,6 +69,7 @@ type ESIToken struct {
AccessToken string AccessToken string
RefreshToken string RefreshToken string
ExpiresAt time.Time ExpiresAt time.Time
WaypointEnabled bool `gorm:"default:true"`
UpdatedAt time.Time UpdatedAt time.Time
CreatedAt time.Time CreatedAt time.Time
} }
@@ -76,6 +77,7 @@ type ESIToken struct {
type CharacterInfo struct { type CharacterInfo struct {
CharacterID int64 `json:"character_id"` CharacterID int64 `json:"character_id"`
CharacterName string `json:"character_name"` CharacterName string `json:"character_name"`
WaypointEnabled bool `json:"waypoint_enabled"`
} }
// CharacterLocation represents a character's current location // CharacterLocation represents a character's current location
@@ -122,36 +124,54 @@ func (s *ESISSO) initDB() error {
// resolveSystemNameByID returns the system name for an ID from the local DB, or empty if not found // resolveSystemNameByID returns the system name for an ID from the local DB, or empty if not found
func (s *ESISSO) resolveSystemNameByID(id int64) string { func (s *ESISSO) resolveSystemNameByID(id int64) string {
if s.db == nil || id == 0 { return "" } if s.db == nil || id == 0 {
return ""
}
var ss SolarSystem var ss SolarSystem
if err := s.db.Select("solarSystemName").First(&ss, "solarSystemID = ?", id).Error; err != nil { return "" } if err := s.db.Select("solarSystemName").First(&ss, "solarSystemID = ?", id).Error; err != nil {
return ""
}
return ss.SolarSystemName return ss.SolarSystemName
} }
// GetCharacterLocations returns current locations for all stored characters // GetCharacterLocations returns current locations for all stored characters
func (s *ESISSO) GetCharacterLocations(ctx context.Context) ([]CharacterLocation, error) { func (s *ESISSO) GetCharacterLocations(ctx context.Context) ([]CharacterLocation, error) {
if s.db == nil { return nil, errors.New("db not initialised") } if s.db == nil {
return nil, errors.New("db not initialised")
}
var tokens []ESIToken var tokens []ESIToken
if err := s.db.Find(&tokens).Error; err != nil { return nil, err } if err := s.db.Find(&tokens).Error; err != nil {
return nil, err
}
out := make([]CharacterLocation, 0, len(tokens)) out := make([]CharacterLocation, 0, len(tokens))
client := &http.Client{ Timeout: 5 * time.Second } client := &http.Client{Timeout: 5 * time.Second}
for i := range tokens { for i := range tokens {
t := &tokens[i] t := &tokens[i]
tok, err := s.ensureAccessTokenFor(ctx, t) tok, err := s.ensureAccessTokenFor(ctx, t)
if err != nil { continue } if err != nil {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/characters/"+strconv.FormatInt(t.CharacterID,10)+"/location", nil) continue
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("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+tok) req.Header.Set("Authorization", "Bearer "+tok)
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { continue } if err != nil {
continue
}
func() { func() {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return } if resp.StatusCode != http.StatusOK {
return
}
var lr esiCharacterLocationResponse var lr esiCharacterLocationResponse
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { return } if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
return
}
name := s.resolveSystemNameByID(lr.SolarSystemID) name := s.resolveSystemNameByID(lr.SolarSystemID)
out = append(out, CharacterLocation{ CharacterID: t.CharacterID, CharacterName: t.CharacterName, SolarSystemID: lr.SolarSystemID, SolarSystemName: name, RetrievedAt: time.Now() }) out = append(out, CharacterLocation{CharacterID: t.CharacterID, CharacterName: t.CharacterName, SolarSystemID: lr.SolarSystemID, SolarSystemName: name, RetrievedAt: time.Now()})
}() }()
} }
return out, nil return out, nil
@@ -527,6 +547,9 @@ func (s *ESISSO) PostWaypointForAll(destinationID int64, clearOthers bool, addTo
} }
var firstErr error var firstErr error
for i := range tokens { for i := range tokens {
if !tokens[i].WaypointEnabled {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
tok, err := s.ensureAccessTokenFor(ctx, &tokens[i]) tok, err := s.ensureAccessTokenFor(ctx, &tokens[i])
cancel() cancel()
@@ -543,6 +566,19 @@ func (s *ESISSO) PostWaypointForAll(destinationID int64, clearOthers bool, addTo
return firstErr 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 { func (s *ESISSO) Status() SSOStatus {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() 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 // PostRouteForAll clears route and posts vias then destination last
func (s *ESISSO) PostRouteForAll(destID int64, viaIDs []int64) error { 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 var tokens []ESIToken
if err := s.db.Find(&tokens).Error; err != nil { return err } if err := s.db.Find(&tokens).Error; err != nil {
// Deduplicate by CharacterID return err
}
// Deduplicate by CharacterID and filter enabled characters
uniq := make(map[int64]ESIToken, len(tokens)) uniq := make(map[int64]ESIToken, len(tokens))
for _, t := range tokens { for _, t := range tokens {
if t.WaypointEnabled {
uniq[t.CharacterID] = t uniq[t.CharacterID] = t
} }
}
uniqueTokens := make([]ESIToken, 0, len(uniq)) 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 mu sync.Mutex
var firstErr error var firstErr error
@@ -690,17 +734,50 @@ func (s *ESISSO) PostRouteForAll(destID int64, viaIDs []int64) error {
tok, err := s.ensureAccessTokenFor(ctx, &t) tok, err := s.ensureAccessTokenFor(ctx, &t)
cancel() cancel()
if err != nil { 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 // Post sequence for this character
if len(viaIDs) > 0 { 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 } if err := s.postWaypointWithToken(tok, viaIDs[0], true, false); err != nil {
for _, id := range viaIDs[1:] { mu.Lock()
if err := s.postWaypointWithToken(tok, id, false, false); err != nil { mu.Lock(); if firstErr == nil { firstErr = err }; mu.Unlock(); return } 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 { } 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]) }(uniqueTokens[i])
} }

View File

@@ -11,7 +11,8 @@ import {
} from '@/components/ui/breadcrumb'; } from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from '@/hooks/use-toast'; 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 { interface HeaderProps {
title: string; title: string;
@@ -21,16 +22,14 @@ interface HeaderProps {
}>; }>;
} }
interface CharacterInfo { character_id: number; character_name: string }
export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => { export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [chars, setChars] = useState<CharacterInfo[]>([]); const [chars, setChars] = useState<main.CharacterInfo[]>([]);
const refreshState = async () => { const refreshState = async () => {
try { try {
const list = await ListCharacters(); const list = await ListCharacters();
setChars((list as any[]).map((c: any) => ({ character_id: c.character_id, character_name: c.character_name }))); setChars(list);
} catch { } } 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 ( return (
<div className="flex-shrink-0 py-4 px-4 border-b border-purple-500/20"> <div className="flex-shrink-0 py-4 px-4 border-b border-purple-500/20">
{breadcrumbs.length > 0 && ( {breadcrumbs.length > 0 && (
@@ -89,9 +99,24 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
<h1 className="text-2xl font-bold text-white">{title}</h1> <h1 className="text-2xl font-bold text-white">{title}</h1>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{chars.length > 0 && ( {chars.length > 0 && (
<div className="flex flex-wrap gap-2 max-w-[50vw] justify-end"> <div
className="grid gap-1 flex-1 justify-end"
style={{
gridTemplateColumns: `repeat(${Math.ceil(chars.length / 2)}, 1fr)`,
gridTemplateRows: 'repeat(2, auto)'
}}
>
{chars.map((c) => ( {chars.map((c) => (
<span key={c.character_id} className="px-2 py-1 rounded-full bg-purple-500/20 text-purple-200 border border-purple-400/40 text-xs whitespace-nowrap"> <span
key={c.character_id}
onClick={() => handleCharacterClick(c)}
className={`px-3 py-1 text-xs cursor-pointer transition-colors text-center overflow-hidden text-ellipsis ${
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} {c.character_name}
</span> </span>
))} ))}

View File

@@ -21,3 +21,5 @@ export function PostRouteForAllByNames(arg1:string,arg2:Array<string>):Promise<v
export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>; export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>;
export function StartESILogin():Promise<string>; export function StartESILogin():Promise<string>;
export function ToggleCharacterWaypointEnabled(arg1:number):Promise<void>;

View File

@@ -41,3 +41,7 @@ export function SetDestinationForAll(arg1, arg2, arg3) {
export function StartESILogin() { export function StartESILogin() {
return window['go']['main']['App']['StartESILogin'](); return window['go']['main']['App']['StartESILogin']();
} }
export function ToggleCharacterWaypointEnabled(arg1) {
return window['go']['main']['App']['ToggleCharacterWaypointEnabled'](arg1);
}

View File

@@ -3,6 +3,7 @@ export namespace main {
export class CharacterInfo { export class CharacterInfo {
character_id: number; character_id: number;
character_name: string; character_name: string;
waypoint_enabled: boolean;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new CharacterInfo(source); return new CharacterInfo(source);
@@ -12,6 +13,7 @@ export namespace main {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.character_id = source["character_id"]; this.character_id = source["character_id"];
this.character_name = source["character_name"]; this.character_name = source["character_name"];
this.waypoint_enabled = source["waypoint_enabled"];
} }
} }
export class CharacterLocation { export class CharacterLocation {