Compare commits
21 Commits
f7879c7ea8
...
master
Author | SHA1 | Date | |
---|---|---|---|
2c0134ed4d | |||
634c72be2d | |||
7efa724631 | |||
22a7d1ad45 | |||
f6450fdafb | |||
7f4ca796aa | |||
0c5d0616e5 | |||
6f3a5dce64 | |||
8575155f4b | |||
41f7d3157f | |||
e72bab7086 | |||
3ca3bf8810 | |||
81713d09fd | |||
2d6af8bfa9 | |||
dad6d79740 | |||
3b20e07b17 | |||
3a4e30d372 | |||
b0ad48985a | |||
c55b3bd882 | |||
c5f7fd483e | |||
c21f82667a |
86
app.go
86
app.go
@@ -28,7 +28,7 @@ func (a *App) startup(ctx context.Context) {
|
||||
|
||||
clientID := os.Getenv("EVE_SSO_CLIENT_ID")
|
||||
if clientID == "" {
|
||||
clientID = "5091f74037374697938384bdbac2698c"
|
||||
clientID = "77c5adb91e46459b874204ceeedb459f"
|
||||
}
|
||||
redirectURI := os.Getenv("EVE_SSO_REDIRECT_URI")
|
||||
if redirectURI == "" {
|
||||
@@ -36,7 +36,46 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Add location read scope so we can fetch character locations
|
||||
a.ssi = NewESISSO(clientID, redirectURI, []string{"esi-ui.write_waypoint.v1", "esi-location.read_location.v1"})
|
||||
a.ssi = NewESISSO(clientID, redirectURI, []string{
|
||||
"esi-location.read_location.v1",
|
||||
"esi-location.read_ship_type.v1",
|
||||
"esi-mail.organize_mail.v1",
|
||||
"esi-mail.read_mail.v1",
|
||||
"esi-mail.send_mail.v1",
|
||||
"esi-skills.read_skills.v1",
|
||||
"esi-skills.read_skillqueue.v1",
|
||||
"esi-wallet.read_character_wallet.v1",
|
||||
"esi-wallet.read_corporation_wallet.v1",
|
||||
"esi-characters.read_contacts.v1",
|
||||
"esi-killmails.read_killmails.v1",
|
||||
"esi-assets.read_assets.v1",
|
||||
"esi-planets.manage_planets.v1",
|
||||
"esi-ui.write_waypoint.v1",
|
||||
"esi-characters.write_contacts.v1",
|
||||
"esi-markets.structure_markets.v1",
|
||||
"esi-characters.read_loyalty.v1",
|
||||
"esi-characters.read_chat_channels.v1",
|
||||
"esi-characters.read_medals.v1",
|
||||
"esi-characters.read_standings.v1",
|
||||
"esi-characters.read_agents_research.v1",
|
||||
"esi-industry.read_character_jobs.v1",
|
||||
"esi-markets.read_character_orders.v1",
|
||||
"esi-characters.read_blueprints.v1",
|
||||
"esi-characters.read_corporation_roles.v1",
|
||||
"esi-location.read_online.v1",
|
||||
"esi-characters.read_fatigue.v1",
|
||||
"esi-killmails.read_corporation_killmails.v1",
|
||||
"esi-wallet.read_corporation_wallets.v1",
|
||||
"esi-characters.read_notifications.v1",
|
||||
"esi-assets.read_corporation_assets.v1",
|
||||
"esi-industry.read_corporation_jobs.v1",
|
||||
"esi-markets.read_corporation_orders.v1",
|
||||
"esi-industry.read_character_mining.v1",
|
||||
"esi-industry.read_corporation_mining.v1",
|
||||
"esi-planets.read_customs_offices.v1",
|
||||
"esi-characters.read_titles.v1",
|
||||
"esi-characters.read_fw_stats.v1",
|
||||
})
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
@@ -124,7 +163,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 +178,47 @@ 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)
|
||||
}
|
||||
|
||||
// GetSystemJumps fetches system jump statistics from ESI
|
||||
func (a *App) GetSystemJumps() ([]SystemJumps, error) {
|
||||
fmt.Printf("🔍 App.GetSystemJumps() called - this should ONLY happen when toggle is ON!\n")
|
||||
if a.ssi == nil {
|
||||
return nil, errors.New("ESI not initialised")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
return a.ssi.GetSystemJumps(ctx)
|
||||
}
|
||||
|
||||
// GetSystemKills fetches system kill statistics from ESI
|
||||
func (a *App) GetSystemKills() ([]SystemKills, error) {
|
||||
fmt.Printf("🔍 App.GetSystemKills() called - this should ONLY happen when toggle is ON!\n")
|
||||
if a.ssi == nil {
|
||||
return nil, errors.New("ESI not initialised")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
return a.ssi.GetSystemKills(ctx)
|
||||
}
|
||||
|
||||
// ResolveSystemIDByName resolves a system name to its ID
|
||||
func (a *App) ResolveSystemIDByName(systemName string) (int64, error) {
|
||||
fmt.Printf("🔍 App.ResolveSystemIDByName() called for system: %s\n", systemName)
|
||||
if a.ssi == nil {
|
||||
return 0, errors.New("ESI not initialised")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
return a.ssi.ResolveSystemIDByName(ctx, systemName)
|
||||
}
|
||||
|
||||
// SystemRegion holds system + region names from local DB
|
||||
type SystemRegion struct {
|
||||
System string `json:"system"`
|
||||
|
344
esi_sso.go
344
esi_sso.go
@@ -63,34 +63,49 @@ 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"`
|
||||
}
|
||||
|
||||
// ESI Statistics data structures
|
||||
type SystemJumps struct {
|
||||
SystemID int64 `json:"system_id"`
|
||||
ShipJumps int64 `json:"ship_jumps"`
|
||||
}
|
||||
|
||||
type SystemKills struct {
|
||||
SystemID int64 `json:"system_id"`
|
||||
ShipKills int64 `json:"ship_kills"`
|
||||
PodKills int64 `json:"pod_kills"`
|
||||
NpcKills int64 `json:"npc_kills"`
|
||||
}
|
||||
|
||||
func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO {
|
||||
@@ -122,39 +137,123 @@ 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
|
||||
}
|
||||
|
||||
// GetSystemJumps fetches system jump statistics from ESI
|
||||
func (s *ESISSO) GetSystemJumps(ctx context.Context) ([]SystemJumps, error) {
|
||||
fmt.Printf("🚀 ESI API REQUEST: Fetching system jumps data from https://esi.evetech.net/v2/universe/system_jumps\n")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/universe/system_jumps", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Compatibility-Date", "2025-08-26")
|
||||
req.Header.Set("X-Tenant", "tranquility")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ESI API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var jumps []SystemJumps
|
||||
if err := json.NewDecoder(resp.Body).Decode(&jumps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("✅ ESI API SUCCESS: Fetched %d system jumps entries\n", len(jumps))
|
||||
return jumps, nil
|
||||
}
|
||||
|
||||
// GetSystemKills fetches system kill statistics from ESI
|
||||
func (s *ESISSO) GetSystemKills(ctx context.Context) ([]SystemKills, error) {
|
||||
fmt.Printf("⚔️ ESI API REQUEST: Fetching system kills data from https://esi.evetech.net/v2/universe/system_kills\n")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/universe/system_kills", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Compatibility-Date", "2025-08-26")
|
||||
req.Header.Set("X-Tenant", "tranquility")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ESI API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var kills []SystemKills
|
||||
if err := json.NewDecoder(resp.Body).Decode(&kills); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("✅ ESI API SUCCESS: Fetched %d system kills entries\n", len(kills))
|
||||
return kills, nil
|
||||
}
|
||||
|
||||
func (s *ESISSO) saveToken() {
|
||||
@@ -209,6 +308,37 @@ func (s *ESISSO) BuildAuthorizeURL() (string, error) {
|
||||
return issuerAuthorizeURL + "?" + q.Encode(), nil
|
||||
}
|
||||
|
||||
func (s *ESISSO) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Callback received: %s %s\n", r.Method, r.URL.String())
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
code := q.Get("code")
|
||||
st := q.Get("state")
|
||||
if code == "" || st == "" || st != s.state {
|
||||
fmt.Printf("Invalid SSO response: code=%s, state=%s, expected_state=%s\n", code, st, s.state)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("Invalid SSO response"))
|
||||
return
|
||||
}
|
||||
fmt.Printf("Exchanging token for code: %s\n", code)
|
||||
if err := s.exchangeToken(r.Context(), code); err != nil {
|
||||
fmt.Printf("Token exchange failed: %v\n", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
|
||||
return
|
||||
}
|
||||
fmt.Printf("Login successful for character: %s (%d)\n", s.characterName, s.characterID)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = io.WriteString(w, "<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>")
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
_ = s.server.Shutdown(context.Background())
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ESISSO) StartCallbackServerAsync() error {
|
||||
u, err := url.Parse(s.redirectURI)
|
||||
if err != nil {
|
||||
@@ -227,29 +357,22 @@ func (s *ESISSO) StartCallbackServerAsync() error {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Add a catch-all handler to debug what's being requested
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("DEBUG: Request received - Method: %s, URL: %s, Path: %s\n", r.Method, r.URL.String(), r.URL.Path)
|
||||
if r.URL.Path == u.Path {
|
||||
// This is our callback, handle it
|
||||
s.handleCallback(w, r)
|
||||
} else {
|
||||
fmt.Printf("DEBUG: 404 - Path %s does not match expected %s\n", r.URL.Path, u.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("Not found"))
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
code := q.Get("code")
|
||||
st := q.Get("state")
|
||||
if code == "" || st == "" || st != s.state {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("Invalid SSO response"))
|
||||
return
|
||||
}
|
||||
if err := s.exchangeToken(r.Context(), code); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, "Login successful. You can close this window.")
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_ = s.server.Shutdown(context.Background())
|
||||
}()
|
||||
s.handleCallback(w, r)
|
||||
})
|
||||
|
||||
ln, err := net.Listen("tcp", hostPort)
|
||||
@@ -257,6 +380,7 @@ func (s *ESISSO) StartCallbackServerAsync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Callback server listening on %s%s\n", hostPort, u.Path)
|
||||
s.server = &http.Server{Handler: mux}
|
||||
go func() { _ = s.server.Serve(ln) }()
|
||||
return nil
|
||||
@@ -494,7 +618,6 @@ func (s *ESISSO) postWaypointWithToken(tok string, destinationID int64, clearOth
|
||||
q.Set("datasource", "tranquility")
|
||||
endpoint := esiBase + "/v2/ui/autopilot/waypoint?" + q.Encode()
|
||||
|
||||
fmt.Printf("ESI: POST waypoint dest=%d clear=%v addToBeginning=%v\n", destinationID, clearOthers, addToBeginning)
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -509,11 +632,9 @@ func (s *ESISSO) postWaypointWithToken(tok string, destinationID int64, clearOth
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("ESI: waypoint set OK", resp.Status)
|
||||
return nil
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("ESI: waypoint failed %s body=%s\n", resp.Status, string(b))
|
||||
return fmt.Errorf("waypoint failed: %s: %s", resp.Status, string(b))
|
||||
}
|
||||
|
||||
@@ -527,6 +648,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 +667,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 +804,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,17 +835,50 @@ 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])
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { SystemView } from "./pages/SystemView";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import "./App.css";
|
||||
import { SearchDialog } from "@/components/SearchDialog";
|
||||
import { SignatureRules } from "./pages/SignatureRules";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -19,6 +20,7 @@ function App() {
|
||||
<Route path="/regions/:region" element={<RegionPage />} />
|
||||
<Route path="/regions/:region/:system" element={<SystemView />} />
|
||||
<Route path="/systems/:system" element={<SystemView />} />
|
||||
<Route path="/settings/signature-rules" element={<SignatureRules />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
<Toaster />
|
||||
|
@@ -11,7 +11,8 @@ import {
|
||||
} 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<CharacterInfo[]>([]);
|
||||
const [chars, setChars] = useState<main.CharacterInfo[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="flex-shrink-0 py-4 px-4 border-b border-purple-500/20">
|
||||
{breadcrumbs.length > 0 && (
|
||||
@@ -88,10 +98,25 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-white">{title}</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" variant="outline" className="border-purple-500/40 text-purple-200" onClick={() => navigate('/settings/signature-rules')}>Rules</Button>
|
||||
{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) => (
|
||||
<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}
|
||||
</span>
|
||||
))}
|
||||
|
@@ -16,6 +16,12 @@ interface MapNodeProps {
|
||||
signatures?: number;
|
||||
isDraggable?: boolean;
|
||||
disableNavigate?: boolean;
|
||||
jumps?: number;
|
||||
kills?: number;
|
||||
showJumps?: boolean;
|
||||
showKills?: boolean;
|
||||
viewBoxWidth?: number; // Add viewBox width for scaling calculations
|
||||
labelScale?: number;
|
||||
}
|
||||
|
||||
export const MapNode: React.FC<MapNodeProps> = ({
|
||||
@@ -33,6 +39,12 @@ export const MapNode: React.FC<MapNodeProps> = ({
|
||||
signatures,
|
||||
isDraggable = false,
|
||||
disableNavigate = false,
|
||||
jumps,
|
||||
kills,
|
||||
showJumps = false,
|
||||
showKills = false,
|
||||
viewBoxWidth = 1200,
|
||||
labelScale = 1,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -177,7 +189,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
|
||||
{/* Node label */}
|
||||
{/* Node label - fixed visual size regardless of zoom */}
|
||||
<text
|
||||
x="0"
|
||||
y={textOffset}
|
||||
@@ -187,23 +199,92 @@ export const MapNode: React.FC<MapNodeProps> = ({
|
||||
fontWeight="bold"
|
||||
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
|
||||
} pointer-events-none select-none`}
|
||||
style={{ textShadow: '2px 2px 4px rgba(0,0,0,0.8)' }}
|
||||
style={{
|
||||
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
|
||||
vectorEffect: 'non-scaling-stroke'
|
||||
}}
|
||||
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
|
||||
>
|
||||
{name} {security !== undefined && (
|
||||
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
|
||||
)}
|
||||
</text>
|
||||
<text
|
||||
x="0"
|
||||
y={textOffset + 15}
|
||||
textAnchor="middle"
|
||||
fill="#a3a3a3"
|
||||
fontSize="12"
|
||||
className="pointer-events-none select-none"
|
||||
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
|
||||
>
|
||||
{signatures !== undefined && signatures > 0 && `📡 ${signatures}`}
|
||||
</text>
|
||||
|
||||
{/* Dynamic text positioning based on what's shown */}
|
||||
{(() => {
|
||||
let currentY = textOffset + 15;
|
||||
const textElements = [];
|
||||
|
||||
// Add signatures if present
|
||||
if (signatures !== undefined && signatures > 0) {
|
||||
textElements.push(
|
||||
<text
|
||||
key="signatures"
|
||||
x="0"
|
||||
y={currentY}
|
||||
textAnchor="middle"
|
||||
fill="#a3a3a3"
|
||||
fontSize="12"
|
||||
className="pointer-events-none select-none"
|
||||
style={{
|
||||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||||
vectorEffect: 'non-scaling-stroke'
|
||||
}}
|
||||
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
|
||||
>
|
||||
📡 {signatures}
|
||||
</text>
|
||||
);
|
||||
currentY += 15;
|
||||
}
|
||||
|
||||
// Add jumps if enabled and present
|
||||
if (showJumps && jumps !== undefined) {
|
||||
textElements.push(
|
||||
<text
|
||||
key="jumps"
|
||||
x="0"
|
||||
y={currentY}
|
||||
textAnchor="middle"
|
||||
fill="#60a5fa"
|
||||
fontSize="10"
|
||||
className="pointer-events-none select-none"
|
||||
style={{
|
||||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||||
vectorEffect: 'non-scaling-stroke'
|
||||
}}
|
||||
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
|
||||
>
|
||||
🚀 {jumps}
|
||||
</text>
|
||||
);
|
||||
currentY += 15;
|
||||
}
|
||||
|
||||
// Add kills if enabled and present
|
||||
if (showKills && kills !== undefined) {
|
||||
textElements.push(
|
||||
<text
|
||||
key="kills"
|
||||
x="0"
|
||||
y={currentY}
|
||||
textAnchor="middle"
|
||||
fill="#f87171"
|
||||
fontSize="10"
|
||||
className="pointer-events-none select-none"
|
||||
style={{
|
||||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||||
vectorEffect: 'non-scaling-stroke'
|
||||
}}
|
||||
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
|
||||
>
|
||||
⚔️ {kills}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
return textElements;
|
||||
})()}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { MapNode } from '@/components/MapNode';
|
||||
import { SystemContextMenu } from '@/components/SystemContextMenu';
|
||||
@@ -11,12 +11,29 @@ import { Header } from './Header';
|
||||
import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames, GetCharacterLocations } from 'wailsjs/go/main/App';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { getSystemsRegions } from '@/utils/systemApi';
|
||||
import { useSystemJumps, useSystemKills, resolveSystemID } from '@/hooks/useSystemStatistics';
|
||||
import { StatisticsToggle } from './StatisticsToggle';
|
||||
|
||||
// Interaction/indicator constants
|
||||
const SELECT_HOLD_MS = 300;
|
||||
const PAN_THRESHOLD_PX = 6;
|
||||
const DRAG_SNAP_DISTANCE = 20;
|
||||
const VIA_WAYPOINT_RING_RADIUS = 14;
|
||||
const VIA_WAYPOINT_RING_COLOR = '#10b981';
|
||||
const INDICATED_RING_RADIUS = 20;
|
||||
const INDICATED_RING_COLOR = '#f59e0b';
|
||||
const INDICATED_RING_ANIM_VALUES = '18;22;18';
|
||||
const INDICATED_RING_ANIM_DUR = '1.2s';
|
||||
const SHIFT_SELECT_STROKE_COLOR = '#60a5fa';
|
||||
const SHIFT_SELECT_FILL_COLOR = 'rgba(96,165,250,0.12)';
|
||||
const SHIFT_SELECT_STROKE_WIDTH = 2;
|
||||
|
||||
interface RegionMapProps {
|
||||
regionName: string;
|
||||
focusSystem?: string;
|
||||
isCompact?: boolean;
|
||||
isWormholeRegion?: boolean;
|
||||
header?: boolean;
|
||||
}
|
||||
|
||||
interface ContextMenuState {
|
||||
@@ -77,7 +94,7 @@ const ensureUniversePositions = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormholeRegion = false }: RegionMapProps) => {
|
||||
export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormholeRegion = false, header = true }: RegionMapProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
@@ -100,6 +117,30 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
const [charLocs, setCharLocs] = useState<Array<{ character_id: number; character_name: string; solar_system_name: string }>>([]);
|
||||
const [focusUntil, setFocusUntil] = useState<number | null>(null);
|
||||
|
||||
// Statistics state - MUST default to false to avoid API spam!
|
||||
const [showJumps, setShowJumps] = useState(false);
|
||||
const [showKills, setShowKills] = useState(false);
|
||||
|
||||
// System ID cache for statistics lookup
|
||||
const [systemIDCache, setSystemIDCache] = useState<Map<string, number>>(new Map());
|
||||
|
||||
|
||||
// New: selection/aim state for left-click aimbot behavior
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [indicatedSystem, setIndicatedSystem] = useState<string | null>(null);
|
||||
const selectTimerRef = useRef<number | null>(null);
|
||||
const downClientPointRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const mouseButtonRef = useRef<number | null>(null);
|
||||
|
||||
// New: shift-drag circle selection state (VIA mode)
|
||||
const [shiftSelecting, setShiftSelecting] = useState(false);
|
||||
const [shiftCenter, setShiftCenter] = useState<Position | null>(null);
|
||||
const [shiftRadius, setShiftRadius] = useState<number>(0);
|
||||
|
||||
// Interaction state machine (lightweight)
|
||||
type InteractionMode = 'idle' | 'holding' | 'panning' | 'selecting' | 'shiftSelecting';
|
||||
const [mode, setMode] = useState<InteractionMode>('idle');
|
||||
|
||||
// When focusSystem changes, set an expiry 20s in the future
|
||||
useEffect(() => {
|
||||
if (focusSystem) {
|
||||
@@ -141,11 +182,36 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
}, [viaMode, viaDest, viaQueue]);
|
||||
|
||||
const { data: rsystems, isLoading, error } = useRegionData(regionName);
|
||||
|
||||
// Fetch statistics data - only when toggles are enabled
|
||||
const { data: jumpsData } = useSystemJumps(showJumps);
|
||||
const { data: killsData } = useSystemKills(showKills);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && error == null && rsystems && rsystems.size > 0)
|
||||
if (!isLoading && error == null && rsystems && rsystems.size > 0) {
|
||||
setSystems(rsystems);
|
||||
|
||||
// Pre-resolve all system IDs for statistics lookup
|
||||
const resolveSystemIDs = async () => {
|
||||
const newCache = new Map<string, number>();
|
||||
for (const systemName of rsystems.keys()) {
|
||||
try {
|
||||
const id = await resolveSystemID(systemName);
|
||||
if (id) {
|
||||
newCache.set(systemName, id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to resolve system ID for ${systemName}:`, error);
|
||||
}
|
||||
}
|
||||
setSystemIDCache(newCache);
|
||||
};
|
||||
|
||||
resolveSystemIDs();
|
||||
}
|
||||
}, [rsystems, isLoading, error]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!systems || systems.size === 0) return;
|
||||
const positions = computeNodePositions(systems);
|
||||
@@ -296,12 +362,17 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
const handleSystemClick = async (systemName: string) => {
|
||||
if (viaMode) {
|
||||
setViaQueue(prev => {
|
||||
if (prev.includes(systemName)) return prev;
|
||||
// toggle behavior: add if missing, remove if present
|
||||
if (prev.includes(systemName)) {
|
||||
const next = prev.filter(n => n !== systemName);
|
||||
toast({ title: 'Waypoint removed', description: systemName });
|
||||
return next;
|
||||
}
|
||||
const next = [...prev, systemName];
|
||||
toast({ title: 'Waypoint queued', description: systemName });
|
||||
return next;
|
||||
});
|
||||
console.log('Queued waypoint:', systemName);
|
||||
toast({ title: 'Waypoint queued', description: systemName });
|
||||
console.log('VIA waypoint toggle:', systemName);
|
||||
return;
|
||||
}
|
||||
if (focusSystem === systemName) return;
|
||||
@@ -381,7 +452,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
const dx = system.x - x;
|
||||
const dy = system.y - y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance < 20) {
|
||||
if (distance < DRAG_SNAP_DISTANCE) {
|
||||
targetSystem = system;
|
||||
}
|
||||
});
|
||||
@@ -437,24 +508,311 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
setTempConnection(null);
|
||||
};
|
||||
|
||||
// Helper: convert client to SVG coords
|
||||
const clientToSvg = (clientX: number, clientY: number) => {
|
||||
if (!svgRef.current) return { x: 0, y: 0 };
|
||||
const pt = svgRef.current.createSVGPoint();
|
||||
pt.x = clientX;
|
||||
pt.y = clientY;
|
||||
const svgPoint = pt.matrixTransform(svgRef.current.getScreenCTM()!.inverse());
|
||||
return { x: svgPoint.x, y: svgPoint.y };
|
||||
};
|
||||
|
||||
// Helper: find nearest system name to SVG point
|
||||
const findNearestSystem = (svgX: number, svgY: number): string | null => {
|
||||
if (systems.size === 0) return null;
|
||||
let nearestName: string | null = null;
|
||||
let nearestDist2 = Number.POSITIVE_INFINITY;
|
||||
systems.forEach((_sys, name) => {
|
||||
const pos = positions[name];
|
||||
if (!pos) return;
|
||||
const dx = pos.x - svgX;
|
||||
const dy = pos.y - svgY;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < nearestDist2) {
|
||||
nearestDist2 = d2;
|
||||
nearestName = name;
|
||||
}
|
||||
});
|
||||
return nearestName;
|
||||
};
|
||||
|
||||
// Create lookup maps for system statistics
|
||||
const jumpsBySystemID = useMemo(() => {
|
||||
if (!jumpsData) return new Map();
|
||||
const map = new Map<number, number>();
|
||||
jumpsData.forEach(jump => {
|
||||
map.set(jump.system_id, jump.ship_jumps);
|
||||
});
|
||||
return map;
|
||||
}, [jumpsData]);
|
||||
|
||||
const killsBySystemID = useMemo(() => {
|
||||
if (!killsData) return new Map();
|
||||
const map = new Map<number, number>();
|
||||
killsData.forEach(kill => {
|
||||
map.set(kill.system_id, kill.ship_kills);
|
||||
});
|
||||
return map;
|
||||
}, [killsData]);
|
||||
|
||||
// Helper functions to get statistics for a system
|
||||
const getSystemJumps = (systemName: string): number | undefined => {
|
||||
if (!showJumps) return undefined;
|
||||
|
||||
const systemID = systemIDCache.get(systemName);
|
||||
if (!systemID) return undefined;
|
||||
|
||||
const jumps = jumpsBySystemID.get(systemID);
|
||||
if (!jumps || jumps === 0) return undefined;
|
||||
|
||||
console.log(`🚀 Found ${jumps} jumps for ${systemName} (ID: ${systemID})`);
|
||||
return jumps;
|
||||
};
|
||||
|
||||
const getSystemKills = (systemName: string): number | undefined => {
|
||||
if (!showKills) return undefined;
|
||||
|
||||
const systemID = systemIDCache.get(systemName);
|
||||
if (!systemID) return undefined;
|
||||
|
||||
const kills = killsBySystemID.get(systemID);
|
||||
if (!kills || kills === 0) return undefined;
|
||||
|
||||
console.log(`⚔️ Found ${kills} kills for ${systemName} (ID: ${systemID})`);
|
||||
return kills;
|
||||
};
|
||||
|
||||
// Commit shift selection: toggle all systems within radius
|
||||
const commitShiftSelection = useCallback(() => {
|
||||
if (!shiftCenter || shiftRadius <= 0) return;
|
||||
const within: string[] = [];
|
||||
Object.keys(positions).forEach(name => {
|
||||
const pos = positions[name];
|
||||
if (!pos) return;
|
||||
const dx = pos.x - shiftCenter.x;
|
||||
const dy = pos.y - shiftCenter.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist <= shiftRadius) within.push(name);
|
||||
});
|
||||
if (within.length === 0) return;
|
||||
setViaQueue(prev => {
|
||||
const prevSet = new Set(prev);
|
||||
const toToggle = new Set(within);
|
||||
// remove toggled ones that were present
|
||||
const kept = prev.filter(n => !toToggle.has(n));
|
||||
// add new ones (those within but not previously present), preserve within order
|
||||
const additions = within.filter(n => !prevSet.has(n));
|
||||
const next = kept.concat(additions);
|
||||
toast({ title: 'VIA toggled', description: `${within.length} systems` });
|
||||
return next;
|
||||
});
|
||||
}, [positions, shiftCenter, shiftRadius]);
|
||||
|
||||
const clearSelectTimer = () => {
|
||||
if (selectTimerRef.current !== null) {
|
||||
window.clearTimeout(selectTimerRef.current);
|
||||
selectTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// const PAN_THRESHOLD_PX = 6; // movement before starting pan
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
if (!svgRef.current) return;
|
||||
setIsPanning(true);
|
||||
|
||||
// If context menu is open, left-click closes it and no selection should happen
|
||||
if (contextMenu) {
|
||||
if (e.button === 0) setContextMenu(null);
|
||||
clearSelectTimer();
|
||||
setIsSelecting(false);
|
||||
setIsPanning(false);
|
||||
setShiftSelecting(false);
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
mouseButtonRef.current = e.button;
|
||||
|
||||
// SHIFT + VIA mode: start circle selection (left button only)
|
||||
if (viaMode && e.shiftKey && e.button === 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const svgPt = clientToSvg(e.clientX, e.clientY);
|
||||
setShiftSelecting(true);
|
||||
setShiftCenter(svgPt);
|
||||
setShiftRadius(0);
|
||||
setMode('shiftSelecting');
|
||||
// cancel any hold-to-select/pan intents
|
||||
setIsSelecting(false);
|
||||
setIsPanning(false);
|
||||
clearSelectTimer();
|
||||
downClientPointRef.current = { x: e.clientX, y: e.clientY };
|
||||
return;
|
||||
}
|
||||
|
||||
// Only left button initiates selection/panning
|
||||
if (e.button !== 0) {
|
||||
clearSelectTimer();
|
||||
setIsSelecting(false);
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// record down point (client) and seed pan origin
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
setLastPanPoint({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||
}, []);
|
||||
downClientPointRef.current = { x: e.clientX, y: e.clientY };
|
||||
|
||||
// initial indicate nearest system under cursor
|
||||
const svgPt = clientToSvg(e.clientX, e.clientY);
|
||||
const near = findNearestSystem(svgPt.x, svgPt.y);
|
||||
setIndicatedSystem(near);
|
||||
|
||||
// start delayed select mode timer
|
||||
setIsSelecting(false);
|
||||
setMode('holding');
|
||||
clearSelectTimer();
|
||||
selectTimerRef.current = window.setTimeout(() => {
|
||||
setIsSelecting(true);
|
||||
setMode('selecting');
|
||||
}, SELECT_HOLD_MS);
|
||||
}, [positions, systems, viaMode, contextMenu]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
if (!isPanning || !svgRef.current) return;
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const currentPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width);
|
||||
const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height);
|
||||
setViewBox(prev => ({ ...prev, x: prev.x + deltaX, y: prev.y + deltaY }));
|
||||
setLastPanPoint(currentPoint);
|
||||
}, [isPanning, lastPanPoint, viewBox.width, viewBox.height]);
|
||||
// if dragging node, delegate
|
||||
if (draggingNode) { handleSvgMouseMove(e); return; }
|
||||
|
||||
const handleMouseUp = useCallback(() => { setIsPanning(false); }, []);
|
||||
if (!svgRef.current) return;
|
||||
|
||||
// Shift selection radius update
|
||||
if (shiftSelecting && shiftCenter) {
|
||||
const svgPt = clientToSvg(e.clientX, e.clientY);
|
||||
const dx = svgPt.x - shiftCenter.x;
|
||||
const dy = svgPt.y - shiftCenter.y;
|
||||
setShiftRadius(Math.sqrt(dx * dx + dy * dy));
|
||||
setMode('shiftSelecting');
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
|
||||
if (isPanning) {
|
||||
const currentPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width);
|
||||
const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height);
|
||||
setViewBox(prev => ({ ...prev, x: prev.x + deltaX, y: prev.y + deltaY }));
|
||||
setLastPanPoint(currentPoint);
|
||||
setMode('panning');
|
||||
return;
|
||||
}
|
||||
|
||||
// determine if we should start panning (from holding)
|
||||
const down = downClientPointRef.current;
|
||||
if (down && !isSelecting) {
|
||||
const dx = e.clientX - down.x;
|
||||
const dy = e.clientY - down.y;
|
||||
const dist2 = dx * dx + dy * dy;
|
||||
if (dist2 > PAN_THRESHOLD_PX * PAN_THRESHOLD_PX) {
|
||||
// user intends to pan; cancel selection
|
||||
clearSelectTimer();
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
setIsPanning(true);
|
||||
setMode('panning');
|
||||
// seed pan origin with current
|
||||
const currentPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
setLastPanPoint(currentPoint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// selection mode: update indicated nearest system as cursor moves
|
||||
if (isSelecting) {
|
||||
const svgPt = clientToSvg(e.clientX, e.clientY);
|
||||
const near = findNearestSystem(svgPt.x, svgPt.y);
|
||||
setIndicatedSystem(near);
|
||||
setMode('selecting');
|
||||
}
|
||||
}, [draggingNode, isPanning, lastPanPoint, viewBox.width, viewBox.height, isSelecting, positions, systems, shiftSelecting, shiftCenter]);
|
||||
|
||||
const handleMouseUp = useCallback((e?: React.MouseEvent) => {
|
||||
// if dragging node, delegate
|
||||
if (draggingNode) { if (e) handleSvgMouseUp(e); return; }
|
||||
|
||||
// If context menu open, left click should just close it; do not select
|
||||
if (contextMenu && mouseButtonRef.current === 0) {
|
||||
setContextMenu(null);
|
||||
clearSelectTimer();
|
||||
setIsPanning(false);
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
setShiftSelecting(false);
|
||||
setShiftCenter(null);
|
||||
setShiftRadius(0);
|
||||
downClientPointRef.current = null;
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Commit shift selection if active (only if left button initiated)
|
||||
if (shiftSelecting) {
|
||||
if (mouseButtonRef.current === 0) {
|
||||
commitShiftSelection();
|
||||
}
|
||||
setShiftSelecting(false);
|
||||
setShiftCenter(null);
|
||||
setShiftRadius(0);
|
||||
clearSelectTimer();
|
||||
setIsPanning(false);
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
downClientPointRef.current = null;
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore non-left button for selection commit
|
||||
if (mouseButtonRef.current !== 0) {
|
||||
clearSelectTimer();
|
||||
setIsPanning(false);
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
downClientPointRef.current = null;
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
clearSelectTimer();
|
||||
|
||||
if (isPanning) {
|
||||
setIsPanning(false);
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// commit selection if any
|
||||
let target = indicatedSystem;
|
||||
if (!target && e && svgRef.current) {
|
||||
const svgPt = clientToSvg(e.clientX, e.clientY);
|
||||
target = findNearestSystem(svgPt.x, svgPt.y);
|
||||
}
|
||||
|
||||
if (target) {
|
||||
handleSystemClick(target);
|
||||
}
|
||||
|
||||
// reset selection state
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
downClientPointRef.current = null;
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
}, [draggingNode, isPanning, indicatedSystem, positions, systems, shiftSelecting, commitShiftSelection, contextMenu]);
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -622,6 +980,27 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onWindowMouseUp = () => {
|
||||
// if shift selection ongoing, commit on global mouseup as well
|
||||
if (shiftSelecting && mouseButtonRef.current === 0) {
|
||||
commitShiftSelection();
|
||||
}
|
||||
clearSelectTimer();
|
||||
setIsPanning(false);
|
||||
setIsSelecting(false);
|
||||
setIndicatedSystem(null);
|
||||
setShiftSelecting(false);
|
||||
setShiftCenter(null);
|
||||
setShiftRadius(0);
|
||||
downClientPointRef.current = null;
|
||||
mouseButtonRef.current = null;
|
||||
setMode('idle');
|
||||
};
|
||||
window.addEventListener('mouseup', onWindowMouseUp);
|
||||
return () => window.removeEventListener('mouseup', onWindowMouseUp);
|
||||
}, [shiftSelecting, commitShiftSelection]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full w-full bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center">
|
||||
@@ -643,22 +1022,24 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative">
|
||||
<Header
|
||||
title={`Region: ${regionName}`}
|
||||
breadcrumbs={[
|
||||
{ label: "Universe", path: "/" },
|
||||
{ label: regionName }
|
||||
]}
|
||||
/>
|
||||
{header && (
|
||||
<Header
|
||||
title={`Region: ${regionName}`}
|
||||
breadcrumbs={[
|
||||
{ label: "Universe", path: "/" },
|
||||
{ label: regionName }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
className={`${(mode === 'selecting' || mode === 'shiftSelecting') ? 'cursor-crosshair' : (mode === 'panning' ? 'cursor-grabbing' : 'cursor-grab')}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={(e) => { if (isPanning) { handleMouseMove(e); } else if (draggingNode) { handleSvgMouseMove(e); } }}
|
||||
onMouseUp={(e) => { if (isPanning) { handleMouseUp(); } else if (draggingNode) { handleSvgMouseUp(e); } }}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={(e) => handleMouseUp(e)}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
onDoubleClick={handleMapDoubleClick}
|
||||
@@ -696,6 +1077,20 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shift selection circle (VIA mode) */}
|
||||
{shiftSelecting && shiftCenter && (
|
||||
<g style={{ pointerEvents: 'none' }}>
|
||||
<circle
|
||||
cx={shiftCenter.x}
|
||||
cy={shiftCenter.y}
|
||||
r={Math.max(shiftRadius, 0)}
|
||||
fill={SHIFT_SELECT_FILL_COLOR}
|
||||
stroke={SHIFT_SELECT_STROKE_COLOR}
|
||||
strokeWidth={SHIFT_SELECT_STROKE_WIDTH}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Render existing systems */}
|
||||
{Array.from(systems.entries()).map(([key, system]) => (
|
||||
<MapNode
|
||||
@@ -703,7 +1098,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
id={system.solarSystemName}
|
||||
name={system.solarSystemName}
|
||||
position={positions[system.solarSystemName] || { x: 0, y: 0 }}
|
||||
onClick={() => handleSystemClick(system.solarSystemName)}
|
||||
onClick={() => { /* handled at svg-level aimbot commit */ }}
|
||||
onDoubleClick={(e) => handleSystemDoubleClick(e, positions[system.solarSystemName])}
|
||||
onDragStart={(e) => handleNodeDragStart(e, system.solarSystemName)}
|
||||
onDrag={handleSvgMouseMove}
|
||||
@@ -714,9 +1109,51 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
signatures={system.signatures}
|
||||
isDraggable={isWormholeRegion}
|
||||
disableNavigate={viaMode}
|
||||
jumps={getSystemJumps(system.solarSystemName)}
|
||||
kills={getSystemKills(system.solarSystemName)}
|
||||
showJumps={showJumps}
|
||||
showKills={showKills}
|
||||
viewBoxWidth={viewBox.width}
|
||||
labelScale={isCompact ? 2.0 : 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* VIA waypoints indicator rings */}
|
||||
{viaMode && viaQueue.map((name) => (
|
||||
positions[name] ? (
|
||||
<g key={`via-${name}`} style={{ pointerEvents: 'none' }}>
|
||||
<circle
|
||||
cx={positions[name].x}
|
||||
cy={positions[name].y}
|
||||
r={VIA_WAYPOINT_RING_RADIUS}
|
||||
fill="none"
|
||||
stroke={VIA_WAYPOINT_RING_COLOR}
|
||||
strokeWidth="3"
|
||||
opacity="0.9"
|
||||
filter="url(#glow)"
|
||||
/>
|
||||
</g>
|
||||
) : null
|
||||
))}
|
||||
|
||||
{/* Indicated (aim) system ring */}
|
||||
{indicatedSystem && positions[indicatedSystem] && (
|
||||
<g style={{ pointerEvents: 'none' }}>
|
||||
<circle
|
||||
cx={positions[indicatedSystem].x}
|
||||
cy={positions[indicatedSystem].y}
|
||||
r={INDICATED_RING_RADIUS}
|
||||
fill="none"
|
||||
stroke={INDICATED_RING_COLOR}
|
||||
strokeWidth="3"
|
||||
opacity="0.9"
|
||||
filter="url(#glow)"
|
||||
>
|
||||
<animate attributeName="r" values={INDICATED_RING_ANIM_VALUES} dur={INDICATED_RING_ANIM_DUR} repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Character location markers */}
|
||||
{charLocs.map((c, idx) => {
|
||||
const pos = positions[c.solar_system_name];
|
||||
@@ -789,6 +1226,16 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statistics Toggle - positioned to avoid overlaps */}
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<StatisticsToggle
|
||||
jumpsEnabled={showJumps}
|
||||
killsEnabled={showKills}
|
||||
onJumpsToggle={setShowJumps}
|
||||
onKillsToggle={setShowKills}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Context Menu */}
|
||||
{contextMenu && (
|
||||
<SystemContextMenu
|
||||
|
@@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { AhoCorasick } from '@/lib/aho';
|
||||
import { ListSystemsWithRegions } from 'wailsjs/go/main/App';
|
||||
import { ListSystemsWithRegions, SetDestinationForAll, ListCharacters, StartESILogin } from 'wailsjs/go/main/App';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
interface SearchResult {
|
||||
system: string;
|
||||
@@ -80,6 +81,39 @@ export const SearchDialog: React.FC = () => {
|
||||
navigate(`/regions/${encodeURIComponent(r.region)}?focus=${encodeURIComponent(r.system)}`);
|
||||
};
|
||||
|
||||
const ensureAnyLoggedIn = async (): Promise<boolean> => {
|
||||
try {
|
||||
const list = await ListCharacters();
|
||||
if (Array.isArray(list) && list.length > 0) return true;
|
||||
await StartESILogin();
|
||||
toast({ title: 'EVE Login', description: 'Complete login in your browser, then retry.' });
|
||||
return false;
|
||||
} catch (e: any) {
|
||||
await StartESILogin();
|
||||
toast({ title: 'EVE Login', description: 'Complete login in your browser, then retry.', variant: 'destructive' });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResultClick = async (e: React.MouseEvent, r: SearchResult) => {
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
if (!(await ensureAnyLoggedIn())) return;
|
||||
await SetDestinationForAll(r.system, true, false);
|
||||
toast({ title: 'Destination set', description: r.system });
|
||||
} catch (err: any) {
|
||||
toast({ title: 'Failed to set destination', description: String(err), variant: 'destructive' });
|
||||
} finally {
|
||||
setOpen(false);
|
||||
setQuery('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
onSelect(r);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-lg bg-slate-900/95 border border-purple-500/40 text-white">
|
||||
@@ -102,7 +136,8 @@ export const SearchDialog: React.FC = () => {
|
||||
<button
|
||||
key={`${r.region}-${r.system}-${idx}`}
|
||||
className="w-full text-left p-3 hover:bg-purple-500/20"
|
||||
onClick={() => onSelect(r)}
|
||||
onClick={(e) => handleResultClick(e, r)}
|
||||
title="Click to open, Shift+Click to set destination"
|
||||
>
|
||||
<div className="text-sm font-medium">{r.system}</div>
|
||||
<div className="text-xs text-slate-300">{r.region}</div>
|
||||
|
@@ -75,10 +75,12 @@ export const SignatureCard = ({ signature, onDelete, onUpdate }: SignatureCardPr
|
||||
{signature.signame || 'Unnamed Signature'}
|
||||
</h3>
|
||||
{signature.note && (
|
||||
<div className="mt-2">
|
||||
<Badge variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-3 py-1 text-sm font-semibold">
|
||||
{signature.note}
|
||||
</Badge>
|
||||
<div className="mt-2 flex flex-wrap gap-1 justify-center">
|
||||
{signature.note.split(';').filter(Boolean).map((note, index) => (
|
||||
<Badge key={index} variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-3 py-1 text-sm font-semibold">
|
||||
{note.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -117,9 +117,13 @@ export const SignatureListItem = ({ signature, onDelete, onUpdate }: SignatureLi
|
||||
)}
|
||||
</h3>
|
||||
{signature.note && (
|
||||
<Badge variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-2 py-0.5 text-sm font-semibold ml-2">
|
||||
{signature.note}
|
||||
</Badge>
|
||||
<div className="flex flex-wrap gap-1 ml-2">
|
||||
{signature.note.split(';').filter(Boolean).map((note, index) => (
|
||||
<Badge key={index} variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-2 py-0.5 text-sm font-semibold">
|
||||
{note.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
44
frontend/src/components/StatisticsToggle.tsx
Normal file
44
frontend/src/components/StatisticsToggle.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
interface StatisticsToggleProps {
|
||||
jumpsEnabled: boolean;
|
||||
killsEnabled: boolean;
|
||||
onJumpsToggle: (enabled: boolean) => void;
|
||||
onKillsToggle: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export const StatisticsToggle: React.FC<StatisticsToggleProps> = ({
|
||||
jumpsEnabled,
|
||||
killsEnabled,
|
||||
onJumpsToggle,
|
||||
onKillsToggle,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-slate-800/90 backdrop-blur-sm rounded-lg p-2 shadow-lg border border-slate-700">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onJumpsToggle(!jumpsEnabled)}
|
||||
className={`p-2 rounded transition-colors ${
|
||||
jumpsEnabled
|
||||
? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
|
||||
: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
|
||||
}`}
|
||||
title={jumpsEnabled ? 'Hide Jumps' : 'Show Jumps'}
|
||||
>
|
||||
🚀
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onKillsToggle(!killsEnabled)}
|
||||
className={`p-2 rounded transition-colors ${
|
||||
killsEnabled
|
||||
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
|
||||
: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
|
||||
}`}
|
||||
title={killsEnabled ? 'Hide Kills' : 'Show Kills'}
|
||||
>
|
||||
⚔️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
41
frontend/src/hooks/useSystemStatistics.ts
Normal file
41
frontend/src/hooks/useSystemStatistics.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as App from 'wailsjs/go/main/App';
|
||||
|
||||
// Helper function to resolve system name to ID
|
||||
export const resolveSystemID = async (systemName: string): Promise<number | null> => {
|
||||
try {
|
||||
const id = await App.ResolveSystemIDByName(systemName);
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to resolve system ID for ${systemName}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useSystemJumps = (enabled: boolean = false) => {
|
||||
console.log('useSystemJumps called with enabled:', enabled);
|
||||
return useQuery({
|
||||
queryKey: ['systemJumps'],
|
||||
queryFn: () => {
|
||||
console.log('🚀 FETCHING SYSTEM JUMPS DATA - API REQUEST MADE!');
|
||||
return App.GetSystemJumps();
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: enabled ? 5 * 60 * 1000 : false, // Only refetch when enabled
|
||||
});
|
||||
};
|
||||
|
||||
export const useSystemKills = (enabled: boolean = false) => {
|
||||
console.log('useSystemKills called with enabled:', enabled);
|
||||
return useQuery({
|
||||
queryKey: ['systemKills'],
|
||||
queryFn: () => {
|
||||
console.log('⚔️ FETCHING SYSTEM KILLS DATA - API REQUEST MADE!');
|
||||
return App.GetSystemKills();
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: enabled ? 5 * 60 * 1000 : false, // Only refetch when enabled
|
||||
});
|
||||
};
|
@@ -11,8 +11,13 @@ export enum Collections {
|
||||
Mfas = "_mfas",
|
||||
Otps = "_otps",
|
||||
Superusers = "_superusers",
|
||||
IndBillitem = "ind_billItem",
|
||||
IndChar = "ind_char",
|
||||
IndJob = "ind_job",
|
||||
IndTransaction = "ind_transaction",
|
||||
Regionview = "regionview",
|
||||
Signature = "signature",
|
||||
SignatureNoteRules = "signature_note_rules",
|
||||
Sigview = "sigview",
|
||||
System = "system",
|
||||
WormholeSystems = "wormholeSystems",
|
||||
@@ -94,6 +99,74 @@ export type SuperusersRecord = {
|
||||
verified?: boolean
|
||||
}
|
||||
|
||||
export type IndBillitemRecord = {
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
name: string
|
||||
quantity: number
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type IndCharRecord = {
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
name: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export enum IndJobStatusOptions {
|
||||
"Planned" = "Planned",
|
||||
"Acquisition" = "Acquisition",
|
||||
"Running" = "Running",
|
||||
"Done" = "Done",
|
||||
"Selling" = "Selling",
|
||||
"Closed" = "Closed",
|
||||
"Tracked" = "Tracked",
|
||||
"Staging" = "Staging",
|
||||
"Inbound" = "Inbound",
|
||||
"Outbound" = "Outbound",
|
||||
"Delivered" = "Delivered",
|
||||
"Queued" = "Queued",
|
||||
}
|
||||
export type IndJobRecord = {
|
||||
billOfMaterials?: RecordIdString[]
|
||||
character?: RecordIdString
|
||||
consumedMaterials?: RecordIdString[]
|
||||
created?: IsoDateString
|
||||
expenditures?: RecordIdString[]
|
||||
id: string
|
||||
income?: RecordIdString[]
|
||||
jobEnd?: IsoDateString
|
||||
jobStart?: IsoDateString
|
||||
outputItem: string
|
||||
outputQuantity: number
|
||||
parallel?: number
|
||||
produced?: number
|
||||
projectedCost?: number
|
||||
projectedRevenue?: number
|
||||
runtime?: number
|
||||
saleEnd?: IsoDateString
|
||||
saleStart?: IsoDateString
|
||||
status: IndJobStatusOptions
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type IndTransactionRecord = {
|
||||
buyer?: string
|
||||
corporation?: string
|
||||
created?: IsoDateString
|
||||
date: IsoDateString
|
||||
id: string
|
||||
itemName: string
|
||||
job?: RecordIdString
|
||||
location?: string
|
||||
quantity: number
|
||||
totalPrice: number
|
||||
unitPrice: number
|
||||
updated?: IsoDateString
|
||||
wallet?: string
|
||||
}
|
||||
|
||||
export type RegionviewRecord = {
|
||||
id: string
|
||||
sigcount?: number
|
||||
@@ -114,6 +187,15 @@ export type SignatureRecord = {
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type SignatureNoteRulesRecord = {
|
||||
created?: IsoDateString
|
||||
enabled?: boolean
|
||||
id: string
|
||||
note: string
|
||||
regex: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type SigviewRecord = {
|
||||
created?: IsoDateString
|
||||
dangerous?: boolean
|
||||
@@ -153,8 +235,13 @@ export type ExternalauthsResponse<Texpand = unknown> = Required<ExternalauthsRec
|
||||
export type MfasResponse<Texpand = unknown> = Required<MfasRecord> & BaseSystemFields<Texpand>
|
||||
export type OtpsResponse<Texpand = unknown> = Required<OtpsRecord> & BaseSystemFields<Texpand>
|
||||
export type SuperusersResponse<Texpand = unknown> = Required<SuperusersRecord> & AuthSystemFields<Texpand>
|
||||
export type IndBillitemResponse<Texpand = unknown> = Required<IndBillitemRecord> & BaseSystemFields<Texpand>
|
||||
export type IndCharResponse<Texpand = unknown> = Required<IndCharRecord> & BaseSystemFields<Texpand>
|
||||
export type IndJobResponse<Texpand = unknown> = Required<IndJobRecord> & BaseSystemFields<Texpand>
|
||||
export type IndTransactionResponse<Texpand = unknown> = Required<IndTransactionRecord> & BaseSystemFields<Texpand>
|
||||
export type RegionviewResponse<Texpand = unknown> = Required<RegionviewRecord> & BaseSystemFields<Texpand>
|
||||
export type SignatureResponse<Texpand = unknown> = Required<SignatureRecord> & BaseSystemFields<Texpand>
|
||||
export type SignatureNoteRulesResponse<Texpand = unknown> = Required<SignatureNoteRulesRecord> & BaseSystemFields<Texpand>
|
||||
export type SigviewResponse<Texpand = unknown> = Required<SigviewRecord> & BaseSystemFields<Texpand>
|
||||
export type SystemResponse<Texpand = unknown> = Required<SystemRecord> & BaseSystemFields<Texpand>
|
||||
export type WormholeSystemsResponse<Texpand = unknown> = Required<WormholeSystemsRecord> & BaseSystemFields<Texpand>
|
||||
@@ -167,8 +254,13 @@ export type CollectionRecords = {
|
||||
_mfas: MfasRecord
|
||||
_otps: OtpsRecord
|
||||
_superusers: SuperusersRecord
|
||||
ind_billItem: IndBillitemRecord
|
||||
ind_char: IndCharRecord
|
||||
ind_job: IndJobRecord
|
||||
ind_transaction: IndTransactionRecord
|
||||
regionview: RegionviewRecord
|
||||
signature: SignatureRecord
|
||||
signature_note_rules: SignatureNoteRulesRecord
|
||||
sigview: SigviewRecord
|
||||
system: SystemRecord
|
||||
wormholeSystems: WormholeSystemsRecord
|
||||
@@ -180,8 +272,13 @@ export type CollectionResponses = {
|
||||
_mfas: MfasResponse
|
||||
_otps: OtpsResponse
|
||||
_superusers: SuperusersResponse
|
||||
ind_billItem: IndBillitemResponse
|
||||
ind_char: IndCharResponse
|
||||
ind_job: IndJobResponse
|
||||
ind_transaction: IndTransactionResponse
|
||||
regionview: RegionviewResponse
|
||||
signature: SignatureResponse
|
||||
signature_note_rules: SignatureNoteRulesResponse
|
||||
sigview: SigviewResponse
|
||||
system: SystemResponse
|
||||
wormholeSystems: WormholeSystemsResponse
|
||||
@@ -196,8 +293,13 @@ export type TypedPocketBase = PocketBase & {
|
||||
collection(idOrName: '_mfas'): RecordService<MfasResponse>
|
||||
collection(idOrName: '_otps'): RecordService<OtpsResponse>
|
||||
collection(idOrName: '_superusers'): RecordService<SuperusersResponse>
|
||||
collection(idOrName: 'ind_billItem'): RecordService<IndBillitemResponse>
|
||||
collection(idOrName: 'ind_char'): RecordService<IndCharResponse>
|
||||
collection(idOrName: 'ind_job'): RecordService<IndJobResponse>
|
||||
collection(idOrName: 'ind_transaction'): RecordService<IndTransactionResponse>
|
||||
collection(idOrName: 'regionview'): RecordService<RegionviewResponse>
|
||||
collection(idOrName: 'signature'): RecordService<SignatureResponse>
|
||||
collection(idOrName: 'signature_note_rules'): RecordService<SignatureNoteRulesResponse>
|
||||
collection(idOrName: 'sigview'): RecordService<SigviewResponse>
|
||||
collection(idOrName: 'system'): RecordService<SystemResponse>
|
||||
collection(idOrName: 'wormholeSystems'): RecordService<WormholeSystemsResponse>
|
||||
|
124
frontend/src/pages/SignatureRules.tsx
Normal file
124
frontend/src/pages/SignatureRules.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import pb from '@/lib/pocketbase';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { SignatureNoteRulesResponse, Collections } from '@/lib/pbtypes';
|
||||
|
||||
export const SignatureRules = () => {
|
||||
const [rules, setRules] = useState<SignatureNoteRulesResponse[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [creating, setCreating] = useState({ regex: '', note: '' });
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await pb.collection(Collections.SignatureNoteRules).getFullList<SignatureNoteRulesResponse>({ batch: 1000, sort: '-updated' });
|
||||
setRules(list);
|
||||
} catch (e: any) {
|
||||
toast({ title: 'Load failed', description: String(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!creating.regex.trim() || !creating.note.trim()) return;
|
||||
try {
|
||||
await pb.collection(Collections.SignatureNoteRules).create({ regex: creating.regex.trim(), note: creating.note.trim(), enabled: true });
|
||||
setCreating({ regex: '', note: '' });
|
||||
await load();
|
||||
toast({ title: 'Rule added', description: 'New rule created.' });
|
||||
} catch (e: any) {
|
||||
toast({ title: 'Create failed', description: String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (id: string, patch: Partial<SignatureNoteRulesResponse>) => {
|
||||
try {
|
||||
await pb.collection(Collections.SignatureNoteRules).update(id, patch);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
toast({ title: 'Update failed', description: String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await pb.collection(Collections.SignatureNoteRules).delete(id);
|
||||
await load();
|
||||
toast({ title: 'Rule deleted' });
|
||||
} catch (e: any) {
|
||||
toast({ title: 'Delete failed', description: String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
|
||||
<div className="h-full flex flex-col">
|
||||
<Header title="Signature Rules" breadcrumbs={[{ label: 'Universe', path: '/' }, { label: 'Signature Rules' }]} />
|
||||
<div className="flex-1 overflow-auto p-4 space-y-4">
|
||||
<div className="bg-black/20 border border-purple-500/30 rounded p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Regex (e.g. ^Angel.*Outpost$|Guristas.*)"
|
||||
value={creating.regex}
|
||||
onChange={e => setCreating({ ...creating, regex: e.target.value })}
|
||||
className="font-mono"
|
||||
/>
|
||||
<Input placeholder="Note/Tag (e.g. 3/10)" value={creating.note} onChange={e => setCreating({ ...creating, note: e.target.value })} />
|
||||
<Button onClick={handleCreate} disabled={loading}>Add Rule</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/20 border border-purple-500/30 rounded">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-slate-300">Enabled</TableHead>
|
||||
<TableHead className="text-slate-300">Regex</TableHead>
|
||||
<TableHead className="text-slate-300">Note</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rules.map(r => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell>
|
||||
<Switch checked={!!r.enabled} onCheckedChange={(v) => handleUpdate(r.id, { enabled: v })} />
|
||||
</TableCell>
|
||||
<TableCell className="max-w-0">
|
||||
<Input
|
||||
type="text"
|
||||
value={r.regex}
|
||||
onChange={e => setRules(prev => prev.map(x => x.id === r.id ? { ...x, regex: e.target.value } : x))}
|
||||
onBlur={e => handleUpdate(r.id, { regex: e.currentTarget.value })}
|
||||
className="font-mono"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-0">
|
||||
<Input
|
||||
value={r.note}
|
||||
onChange={e => setRules(prev => prev.map(x => x.id === r.id ? { ...x, note: e.target.value } : x))}
|
||||
onBlur={e => handleUpdate(r.id, { note: e.currentTarget.value })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="destructive" onClick={() => handleDelete(r.id)}>Delete</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -8,7 +8,7 @@ import { Header } from "@/components/Header";
|
||||
import { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
|
||||
import { getSystemId } from "@/utils/systemApi";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import { SigviewRecord as Signature, SignatureRecord } from "@/lib/pbtypes";
|
||||
import { SigviewRecord as Signature, SignatureRecord, SignatureNoteRulesResponse, Collections } from "@/lib/pbtypes";
|
||||
|
||||
export const SystemView = () => {
|
||||
const { system, region } = useParams();
|
||||
@@ -163,12 +163,17 @@ export const SystemView = () => {
|
||||
|
||||
try {
|
||||
const systemId = await getSystemId(system);
|
||||
let rules: Array<Pick<SignatureNoteRulesResponse, 'regex' | 'note' | 'enabled'>> = [];
|
||||
try {
|
||||
const list = await pb.collection(Collections.SignatureNoteRules).getFullList<SignatureNoteRulesResponse>({ batch: 1000 });
|
||||
rules = list.filter(r => r.enabled).map(r => ({ regex: r.regex, note: r.note, enabled: r.enabled }));
|
||||
} catch { }
|
||||
const lines = pastedText.trim().split('\n').filter(line => line.trim());
|
||||
const parsedSignatures: Omit<Signature, 'id'>[] = [];
|
||||
|
||||
// Parse all signatures
|
||||
for (const line of lines) {
|
||||
const parsed = parseSignature(line);
|
||||
const parsed = parseSignature(line, rules);
|
||||
if (parsed) {
|
||||
parsedSignatures.push({
|
||||
...parsed,
|
||||
@@ -276,6 +281,7 @@ export const SystemView = () => {
|
||||
regionName={region}
|
||||
focusSystem={system}
|
||||
isCompact={true}
|
||||
header={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,52 +1,7 @@
|
||||
import { SigviewRecord as Signature } from "@/lib/pbtypes";
|
||||
import { SigviewRecord as Signature, SignatureNoteRulesResponse } from "@/lib/pbtypes";
|
||||
|
||||
const oneOutOfTen = [
|
||||
"Minmatar Contracted Bio-Farm",
|
||||
"Old Meanie - Cultivation Center",
|
||||
"Pith Robux Asteroid Mining & Co.",
|
||||
"Sansha Military Outpost",
|
||||
"Serpentis Drug Outlet",
|
||||
];
|
||||
const twoOutOfTen = [
|
||||
"Angel Creo-Corp Mining",
|
||||
"Blood Raider Human Farm",
|
||||
"Pith Merchant Depot",
|
||||
"Sansha Acclimatization Facility",
|
||||
"Serpentis Live Cargo Distribution Facilities",
|
||||
"Rogue Drone Infestation Sprout",
|
||||
];
|
||||
const threeOutOfTen = [
|
||||
"Angel Repurposed Outpost",
|
||||
"Blood Raider Intelligence Collection Point",
|
||||
"Guristas Guerilla Grounds",
|
||||
"Sansha's Command Relay Outpost",
|
||||
"Serpentis Narcotic Warehouses",
|
||||
"Rogue Drone Asteroid Infestation",
|
||||
];
|
||||
const fourOutOfTen = [
|
||||
"Angel Cartel Occupied Mining Colony",
|
||||
"Mul-Zatah Monastery",
|
||||
"Guristas Scout Outpost",
|
||||
"Sansha's Nation Occupied Mining Colony",
|
||||
"Serpentis Phi-Outpost",
|
||||
"Drone Infested Mine",
|
||||
];
|
||||
const fiveOutOfTen = [
|
||||
"Angel's Red Light District",
|
||||
"Blood Raider Psychotropics Depot",
|
||||
"Guristas Hallucinogen Supply Waypoint",
|
||||
"Sansha's Nation Neural Paralytic Facility",
|
||||
"Serpentis Corporation Hydroponics Site",
|
||||
"Outgrowth Rogue Drone Hive",
|
||||
];
|
||||
function isFourOutOfTen(signature: string): boolean {
|
||||
return fourOutOfTen.some((s) => signature.includes(s));
|
||||
}
|
||||
function isFiveOutOfTen(signature: string): boolean {
|
||||
return fiveOutOfTen.some((s) => signature.includes(s));
|
||||
}
|
||||
|
||||
export const parseSignature = (text: string): Omit<Signature, 'system' | 'id' | 'sysid'> | null => {
|
||||
export const parseSignature = (text: string, rules?: Array<Pick<SignatureNoteRulesResponse, 'regex' | 'note' | 'enabled'>>): Omit<Signature, 'system' | 'id' | 'sysid'> | null => {
|
||||
const parts = text.split('\t');
|
||||
if (parts.length < 4) return null;
|
||||
|
||||
@@ -56,16 +11,26 @@ export const parseSignature = (text: string): Omit<Signature, 'system' | 'id' |
|
||||
return null;
|
||||
}
|
||||
|
||||
let note = "";
|
||||
const isFour = isFourOutOfTen(parts[3]);
|
||||
if (isFour) {
|
||||
note = "4/10";
|
||||
}
|
||||
const isFive = isFiveOutOfTen(parts[3]);
|
||||
if (isFive) {
|
||||
note = "5/10";
|
||||
const appliedNotes: string[] = [];
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
for (const rule of rules) {
|
||||
if (rule && rule.enabled) {
|
||||
try {
|
||||
const re = new RegExp(rule.regex, 'i');
|
||||
if (re.test(parts[3])) {
|
||||
appliedNotes.push(rule.note);
|
||||
}
|
||||
} catch {
|
||||
// invalid regex - ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dedupedNotes = Array.from(new Set(appliedNotes)).filter(Boolean);
|
||||
const note = dedupedNotes.join(';');
|
||||
|
||||
return {
|
||||
identifier: parts[0],
|
||||
type: parts[2],
|
||||
|
8
frontend/wailsjs/go/main/App.d.ts
vendored
8
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -10,6 +10,10 @@ export function ESILoginStatus():Promise<string>;
|
||||
|
||||
export function GetCharacterLocations():Promise<Array<main.CharacterLocation>>;
|
||||
|
||||
export function GetSystemJumps():Promise<Array<main.SystemJumps>>;
|
||||
|
||||
export function GetSystemKills():Promise<Array<main.SystemKills>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function ListCharacters():Promise<Array<main.CharacterInfo>>;
|
||||
@@ -18,6 +22,10 @@ export function ListSystemsWithRegions():Promise<Array<main.SystemRegion>>;
|
||||
|
||||
export function PostRouteForAllByNames(arg1:string,arg2:Array<string>):Promise<void>;
|
||||
|
||||
export function ResolveSystemIDByName(arg1:string):Promise<number>;
|
||||
|
||||
export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>;
|
||||
|
||||
export function StartESILogin():Promise<string>;
|
||||
|
||||
export function ToggleCharacterWaypointEnabled(arg1:number):Promise<void>;
|
||||
|
@@ -18,6 +18,14 @@ export function GetCharacterLocations() {
|
||||
return window['go']['main']['App']['GetCharacterLocations']();
|
||||
}
|
||||
|
||||
export function GetSystemJumps() {
|
||||
return window['go']['main']['App']['GetSystemJumps']();
|
||||
}
|
||||
|
||||
export function GetSystemKills() {
|
||||
return window['go']['main']['App']['GetSystemKills']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
@@ -34,6 +42,10 @@ export function PostRouteForAllByNames(arg1, arg2) {
|
||||
return window['go']['main']['App']['PostRouteForAllByNames'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ResolveSystemIDByName(arg1) {
|
||||
return window['go']['main']['App']['ResolveSystemIDByName'](arg1);
|
||||
}
|
||||
|
||||
export function SetDestinationForAll(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetDestinationForAll'](arg1, arg2, arg3);
|
||||
}
|
||||
@@ -41,3 +53,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);
|
||||
}
|
||||
|
@@ -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 {
|
||||
@@ -53,6 +55,38 @@ export namespace main {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class SystemJumps {
|
||||
system_id: number;
|
||||
ship_jumps: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SystemJumps(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.system_id = source["system_id"];
|
||||
this.ship_jumps = source["ship_jumps"];
|
||||
}
|
||||
}
|
||||
export class SystemKills {
|
||||
system_id: number;
|
||||
ship_kills: number;
|
||||
pod_kills: number;
|
||||
npc_kills: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SystemKills(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.system_id = source["system_id"];
|
||||
this.ship_kills = source["ship_kills"];
|
||||
this.pod_kills = source["pod_kills"];
|
||||
this.npc_kills = source["npc_kills"];
|
||||
}
|
||||
}
|
||||
export class SystemRegion {
|
||||
system: string;
|
||||
region: string;
|
||||
|
Reference in New Issue
Block a user