feat(go, ts): add system-region mapping and highlight functionality

This commit is contained in:
2025-08-10 22:26:02 +02:00
parent 2561cd7d30
commit 97178bc9a5
7 changed files with 103 additions and 38 deletions

22
app.go
View File

@@ -138,3 +138,25 @@ func (a *App) GetCharacterLocations() ([]CharacterLocation, error) {
defer cancel() defer cancel()
return a.ssi.GetCharacterLocations(ctx) return a.ssi.GetCharacterLocations(ctx)
} }
// SystemRegion holds system + region names from local DB
type SystemRegion struct {
System string `json:"system"`
Region string `json:"region"`
}
// ListSystemsWithRegions returns all solar system names and their regions from the local SQLite DB
func (a *App) ListSystemsWithRegions() ([]SystemRegion, error) {
if a.ssi == nil || a.ssi.db == nil {
return nil, errors.New("db not initialised")
}
var rows []SystemRegion
// mapSolarSystems has regionID; mapRegions has regionName
q := `SELECT s.solarSystemName AS system, r.regionName AS region
FROM mapSolarSystems s
JOIN mapRegions r ON r.regionID = s.regionID`
if err := a.ssi.db.Raw(q).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}

View File

@@ -98,6 +98,25 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
const [offRegionIndicators, setOffRegionIndicators] = useState<OffIndicator[]>([]); const [offRegionIndicators, setOffRegionIndicators] = useState<OffIndicator[]>([]);
const [meanNeighborAngle, setMeanNeighborAngle] = useState<Record<string, number>>({}); const [meanNeighborAngle, setMeanNeighborAngle] = useState<Record<string, number>>({});
const [charLocs, setCharLocs] = useState<Array<{ character_id: number; character_name: string; solar_system_name: string }>>([]); const [charLocs, setCharLocs] = useState<Array<{ character_id: number; character_name: string; solar_system_name: string }>>([]);
const [focusUntil, setFocusUntil] = useState<number | null>(null);
// When focusSystem changes, set an expiry 20s in the future
useEffect(() => {
if (focusSystem) {
setFocusUntil(Date.now() + 20000);
}
}, [focusSystem]);
// Timer to clear focus after expiry
useEffect(() => {
if (!focusUntil) return;
const id = setInterval(() => {
if (Date.now() > focusUntil) {
setFocusUntil(null);
}
}, 500);
return () => clearInterval(id);
}, [focusUntil]);
useEffect(() => { useEffect(() => {
const onKeyDown = async (e: KeyboardEvent) => { const onKeyDown = async (e: KeyboardEvent) => {
@@ -700,27 +719,35 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
})} })}
{/* Highlight focused system */} {/* Highlight focused system */}
{focusSystem && positions[focusSystem] && ( {focusSystem && focusUntil && Date.now() <= focusUntil && positions[focusSystem] && (
<circle <g>
cx={positions[focusSystem].x} <circle
cy={positions[focusSystem].y} cx={positions[focusSystem].x}
r="15" cy={positions[focusSystem].y}
fill="none" r="20"
stroke="#a855f7" fill="none"
strokeWidth="3" stroke="#f472b6"
strokeDasharray="5,5" strokeWidth="4"
opacity="0.8" strokeDasharray="6,6"
> opacity="0.95"
<animateTransform filter="url(#glow)"
attributeName="transform" >
attributeType="XML" <animate
type="rotate" attributeName="r"
from={`0 ${positions[focusSystem].x} ${positions[focusSystem].y}`} values="18;24;18"
to={`360 ${positions[focusSystem].x} ${positions[focusSystem].y}`} dur="1.8s"
dur="10s" repeatCount="indefinite"
repeatCount="indefinite" />
</circle>
<circle
cx={positions[focusSystem].x}
cy={positions[focusSystem].y}
r="8"
fill="#f472b6"
opacity="0.8"
/> />
</circle> <text x={positions[focusSystem].x + 14} y={positions[focusSystem].y - 14} fontSize="10" fill="#ffffff" stroke="#0f172a" strokeWidth="2" paintOrder="stroke">{focusSystem}</text>
</g>
)} )}
</svg> </svg>

View File

@@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { AhoCorasick } from '@/lib/aho'; import { AhoCorasick } from '@/lib/aho';
import pb from '@/lib/pocketbase';
interface SearchResult { interface SearchResult {
system: string; system: string;
@@ -11,26 +10,22 @@ interface SearchResult {
} }
async function loadAllSystems(): Promise<Array<SearchResult>> { async function loadAllSystems(): Promise<Array<SearchResult>> {
// Fetch system names with regions from PocketBase, paginated and minimal fields // Fetch from Go (Wails): local SQLite DB via App.ListSystemsWithRegions
const perPage = 1000; try {
let page = 1; const list = await (window as any)?.go?.main?.App?.ListSystemsWithRegions?.();
const out: Array<SearchResult> = []; if (Array.isArray(list)) {
const seen = new Set<string>(); const seen = new Set<string>();
// loop pages until fewer than perPage items const out: Array<SearchResult> = [];
while (true) { for (const item of list) {
const res = await pb.collection('regionview').getList(page, perPage, { fields: 'sysname,sysregion' }); const system = String(item.system);
for (const item of res.items as any[]) { if (seen.has(system)) continue;
const system: string = item.sysname;
const region: string = item.sysregion;
if (!seen.has(system)) {
seen.add(system); seen.add(system);
out.push({ system, region }); out.push({ system, region: String(item.region) });
} }
return out;
} }
if (res.items.length < perPage) break; } catch (_) { /* noop */ }
page += 1; return [];
}
return out;
} }
export const SearchDialog: React.FC = () => { export const SearchDialog: React.FC = () => {

1
frontend/wails.json Normal file
View File

@@ -0,0 +1 @@

View File

@@ -14,6 +14,8 @@ export function Greet(arg1:string):Promise<string>;
export function ListCharacters():Promise<Array<main.CharacterInfo>>; export function ListCharacters():Promise<Array<main.CharacterInfo>>;
export function ListSystemsWithRegions():Promise<Array<main.SystemRegion>>;
export function PostRouteForAllByNames(arg1:string,arg2:Array<string>):Promise<void>; export function PostRouteForAllByNames(arg1:string,arg2:Array<string>):Promise<void>;
export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>; export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>;

View File

@@ -26,6 +26,10 @@ export function ListCharacters() {
return window['go']['main']['App']['ListCharacters'](); return window['go']['main']['App']['ListCharacters']();
} }
export function ListSystemsWithRegions() {
return window['go']['main']['App']['ListSystemsWithRegions']();
}
export function PostRouteForAllByNames(arg1, arg2) { export function PostRouteForAllByNames(arg1, arg2) {
return window['go']['main']['App']['PostRouteForAllByNames'](arg1, arg2); return window['go']['main']['App']['PostRouteForAllByNames'](arg1, arg2);
} }

View File

@@ -53,6 +53,20 @@ export namespace main {
return a; return a;
} }
} }
export class SystemRegion {
system: string;
region: string;
static createFrom(source: any = {}) {
return new SystemRegion(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.system = source["system"];
this.region = source["region"];
}
}
} }