Enable toggling waypoint sending characters
This commit is contained in:
10
app.go
10
app.go
@@ -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"`
|
||||||
|
121
esi_sso.go
121
esi_sso.go
@@ -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])
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
2
frontend/wailsjs/go/main/App.d.ts
vendored
2
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -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>;
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user