mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-09-22 21:16:11 +00:00
feat: enhance character activty and summmarize by user (#206)
This commit is contained in:
@@ -131,6 +131,21 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -8,13 +8,20 @@ import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/com
|
||||
import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx';
|
||||
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
||||
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
||||
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
import { TrackAndFollow } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/TrackAndFollow';
|
||||
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
||||
import { useTrackAndFollowHandlers } from './hooks/useTrackAndFollowHandlers';
|
||||
|
||||
export interface MapRootContentProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
const { interfaceSettings, data } = useMapRootState();
|
||||
const { isShowMenu } = interfaceSettings;
|
||||
const { showCharacterActivity, showTrackAndFollow } = data;
|
||||
const { handleHideCharacterActivity } = useCharacterActivityHandlers();
|
||||
const { handleHideTracking } = useTrackAndFollowHandlers();
|
||||
|
||||
const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`;
|
||||
|
||||
@@ -49,7 +56,11 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
</div>
|
||||
)}
|
||||
<OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} />
|
||||
<MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} />
|
||||
{showMapSettings && <MapSettings visible={showMapSettings} onHide={() => setShowMapSettings(false)} />}
|
||||
{showCharacterActivity && (
|
||||
<CharacterActivity visible={showCharacterActivity} onHide={handleHideCharacterActivity} />
|
||||
)}
|
||||
{showTrackAndFollow && <TrackAndFollow visible={showTrackAndFollow} onHide={handleHideTracking} />}
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
/* Character Activity Component Styles */
|
||||
.characterActivity {
|
||||
&Dialog {
|
||||
background-color: var(--surface-card);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
width: 80vw;
|
||||
max-width: 650px;
|
||||
|
||||
/* Note: Overlay z-index settings are handled in fix-dialog.scss */
|
||||
|
||||
.p-dialog-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-header-icon {
|
||||
color: var(--text-color-secondary);
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0 none;
|
||||
outline-offset: 0;
|
||||
box-shadow: 0 0 0 0.2rem var(--focus-ring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced close button styling */
|
||||
.p-dialog-header-close {
|
||||
color: var(--text-color) !important;
|
||||
background: var(--surface-hover) !important;
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
border-radius: 50% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
position: relative !important;
|
||||
margin-left: 0.5rem !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.p-dialog-header-close:hover {
|
||||
background: var(--surface-hover) !important;
|
||||
color: var(--primary-color) !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.p-dialog-header-close:focus {
|
||||
box-shadow: 0 0 0 0.2rem var(--primary-color-light) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.p-dialog-header-close-icon {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-content {
|
||||
padding: 0;
|
||||
background-color: var(--surface-card);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
height: calc(100% - 70px);
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
.p-dialog-footer {
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top: 1px solid var(--surface-border);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set a fixed height for the container so scrolling works properly */
|
||||
&Container {
|
||||
width: 100%;
|
||||
height: 400px; // fixed height to match the DataTable's scrollHeight
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&Datatable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
flex: 1;
|
||||
table-layout: fixed !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.p-datatable-wrapper {
|
||||
border-radius: 0;
|
||||
overflow: auto !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--surface-section);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable-table {
|
||||
width: 100% !important;
|
||||
table-layout: fixed !important;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.p-datatable-thead > tr > th {
|
||||
background-color: var(--surface-section);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: 2px 8px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
line-height: 1.333;
|
||||
font-size: 12px;
|
||||
height: 24px !important;
|
||||
|
||||
&.headerCharacter {
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&.headerStandard {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Row styling */
|
||||
.p-datatable-tbody > tr {
|
||||
background-color: var(--surface-card);
|
||||
transition: background-color 0.2s;
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* Cell styling */
|
||||
& > td {
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
color: var(--text-color);
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty message styling */
|
||||
.p-datatable-emptymessage td {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterNameContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.nameText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
display: inline;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.8;
|
||||
display: inline;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
max-width: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Cell styling */
|
||||
.characterNameCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.activityValueCell {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
font-variant-numeric: tabular-nums;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
/* Column styles */
|
||||
.characterColumn {
|
||||
min-width: 200px;
|
||||
width: 40%;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.numericColumn {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Empty state message */
|
||||
.emptyMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Loading container */
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
margin-top: 1rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.errorMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
/* Table row compact style */
|
||||
.tableRowCompact {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.333 !important;
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import classes from './CharacterActivity.module.scss';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
|
||||
/**
|
||||
* Summary of a character's activity
|
||||
*/
|
||||
export interface ActivitySummary {
|
||||
character_id: string;
|
||||
character_name: string;
|
||||
corporation_ticker: string;
|
||||
alliance_ticker?: string;
|
||||
portrait_url: string;
|
||||
passages: number;
|
||||
connections: number;
|
||||
signatures: number;
|
||||
user_id?: string;
|
||||
user_name?: string;
|
||||
is_user?: boolean;
|
||||
}
|
||||
|
||||
interface CharacterActivityProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const getRowClassName = () => classes.tableRowCompact;
|
||||
|
||||
const renderCharacterTemplate = (rowData: ActivitySummary) => {
|
||||
return (
|
||||
<div className={classes.characterNameCell}>
|
||||
<div className={classes.characterInfo}>
|
||||
<div className={classes.characterPortrait}>
|
||||
<img src={rowData.portrait_url} alt={rowData.character_name} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className={classes.characterNameContainer}>
|
||||
<div className={classes.characterName}>
|
||||
{rowData.is_user ? (
|
||||
<>
|
||||
<span className={classes.nameText}>{rowData.user_name}</span>
|
||||
<span className={classes.corporationTicker}>[{rowData.corporation_ticker}]</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={classes.nameText}>{rowData.character_name}</span>
|
||||
<span className={classes.corporationTicker}>[{rowData.corporation_ticker}]</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
|
||||
return <div className={classes.activityValueCell}>{rowData[field] as number}</div>;
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Character Activity</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component that displays character activity in a dialog.
|
||||
*/
|
||||
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
|
||||
const { data } = useMapRootState();
|
||||
const { characterActivityData } = data;
|
||||
const [localActivity, setLocalActivity] = useState<ActivitySummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Use the new structure directly
|
||||
const activity = useMemo(() => {
|
||||
return characterActivityData?.activity || [];
|
||||
}, [characterActivityData]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalActivity(activity);
|
||||
setLoading(characterActivityData?.loading !== false);
|
||||
}, [activity, characterActivityData]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={renderHeader}
|
||||
visible={visible}
|
||||
className={classes.characterActivityDialog}
|
||||
onHide={onHide}
|
||||
dismissableMask
|
||||
draggable={false}
|
||||
resizable={false}
|
||||
closable
|
||||
modal={true}
|
||||
>
|
||||
<div className={classes.characterActivityContainer}>
|
||||
{loading && (
|
||||
<div className={classes.loadingContainer}>
|
||||
<ProgressSpinner style={{ width: '50px', height: '50px' }} strokeWidth="4" />
|
||||
<div className={classes.loadingText}>Loading character activity data...</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && localActivity.length === 0 && (
|
||||
<div className={classes.emptyMessage}>No character activity data available</div>
|
||||
)}
|
||||
{!loading && localActivity.length > 0 && (
|
||||
<DataTable
|
||||
value={localActivity}
|
||||
className={classes.characterActivityDatatable}
|
||||
scrollable
|
||||
scrollHeight="400px"
|
||||
emptyMessage="No character activity data available"
|
||||
sortField="passages"
|
||||
sortOrder={-1}
|
||||
responsiveLayout="scroll"
|
||||
size="small"
|
||||
rowClassName={getRowClassName}
|
||||
rowHover
|
||||
>
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
body={renderCharacterTemplate}
|
||||
sortable
|
||||
className={classes.characterColumn}
|
||||
headerClassName={`${classes.headerCharacter} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="passages"
|
||||
header="Passages"
|
||||
body={rowData => renderValueTemplate(rowData, 'passages')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="connections"
|
||||
header="Connections"
|
||||
body={rowData => renderValueTemplate(rowData, 'connections')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="signatures"
|
||||
header="Signatures"
|
||||
body={rowData => renderValueTemplate(rowData, 'signatures')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
</DataTable>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+23
-3
@@ -22,8 +22,15 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.addCharacter,
|
||||
data: null,
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
const handleShowActivity = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
@@ -36,6 +43,12 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
command: handleAddCharacter,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Character Activity',
|
||||
icon: 'pi pi-chart-bar',
|
||||
command: handleShowActivity,
|
||||
visible: canTrackCharacters,
|
||||
},
|
||||
{
|
||||
label: 'On the map',
|
||||
icon: 'pi pi-hashtag',
|
||||
@@ -61,7 +74,14 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
},
|
||||
] as MenuItem[]
|
||||
).filter(item => item.visible);
|
||||
}, [canTrackCharacters, handleAddCharacter, onShowMapSettings, onShowOnTheMap, setInterfaceSettings]);
|
||||
}, [
|
||||
canTrackCharacters,
|
||||
handleAddCharacter,
|
||||
handleShowActivity,
|
||||
onShowMapSettings,
|
||||
onShowOnTheMap,
|
||||
setInterfaceSettings,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="ml-1">
|
||||
|
||||
+4
-4
@@ -40,7 +40,7 @@ export type UserSettingsRemote = {
|
||||
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
|
||||
|
||||
export interface MapSettingsProps {
|
||||
show: boolean;
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ const THEME_SETTING: SettingsListItem = {
|
||||
options: THEME_OPTIONS,
|
||||
};
|
||||
|
||||
export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
|
||||
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
|
||||
@@ -213,12 +213,12 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
header="Map user settings"
|
||||
visible={show}
|
||||
visible
|
||||
draggable={false}
|
||||
style={{ width: '550px' }}
|
||||
onShow={handleShow}
|
||||
onHide={() => {
|
||||
if (!show) return;
|
||||
if (!visible) return;
|
||||
setActiveIndex(0);
|
||||
onHide();
|
||||
}}
|
||||
|
||||
@@ -21,10 +21,11 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
|
||||
|
||||
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
const handleShowTracking = useCallback(() => {
|
||||
// Use the OutCommand pattern for showing the tracking dialog
|
||||
outCommand({
|
||||
type: OutCommand.addCharacter,
|
||||
data: null,
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
@@ -63,22 +64,25 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={handleAddCharacter}
|
||||
onClick={handleShowTracking}
|
||||
id="show-tracking-button"
|
||||
>
|
||||
<i className="pi pi-user-plus"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
{canTrackCharacters && (
|
||||
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={onShowOnTheMap}
|
||||
>
|
||||
<i className="pi pi-hashtag"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
<>
|
||||
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={onShowOnTheMap}
|
||||
>
|
||||
<i className="pi pi-hashtag"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
/* TrackAndFollow Dialog Styles */
|
||||
.trackFollowDialog {
|
||||
background-color: var(--surface-card);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
width: 500px;
|
||||
|
||||
:global {
|
||||
/* Dialog header */
|
||||
.p-dialog-header {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-header-close {
|
||||
color: var(--text-color);
|
||||
background: transparent;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 0.2rem var(--primary-color-light);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog content */
|
||||
.p-dialog-content {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--surface-card);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
height: calc(100% - 70px);
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dialog mask */
|
||||
.p-dialog-mask.p-component-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Character grid styles */
|
||||
.characterGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.characterGridHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
background-color: var(--surface-section);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
padding: 0.75rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.characterGridBody {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.characterGridRow {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: 0.5rem 0;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gridCellTrack,
|
||||
.gridCellFollow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gridCellCharacter {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterDetails {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.characterCorp {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Empty state message */
|
||||
.emptyMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom Checkbox and Radio Button Styles */
|
||||
.wdCheckbox, .wdRadio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--surface-border);
|
||||
background-color: var(--surface-card);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.wdCheckbox {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wdCheckboxChecked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.wdCheckboxIcon {
|
||||
color: var(--primary-color-text);
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wdRadio {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.wdRadioChecked {
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.wdRadioDot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trackingCell {
|
||||
.p-checkbox {
|
||||
.p-checkbox-box {
|
||||
background-color: var(--surface-card);
|
||||
border: 2px solid var(--surface-border);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.p-highlight {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.p-checkbox-icon {
|
||||
color: var(--primary-color-text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.followCell {
|
||||
.p-radiobutton {
|
||||
.p-radiobutton-box {
|
||||
background-color: var(--surface-card);
|
||||
border: 2px solid var(--surface-border);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.p-highlight {
|
||||
background-color: var(--surface-card);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.p-radiobutton-icon {
|
||||
background-color: var(--primary-color);
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 640px) {
|
||||
.trackingCharacterHeader {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.trackingCharacterRow {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radioContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portraitImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
margin-left: 0.5rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
margin-left: 0.25rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.hoverRow:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { VirtualScroller } from 'primereact/virtualscroller';
|
||||
import classes from './TrackAndFollow.module.scss';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
|
||||
import { TrackingCharacter } from './types';
|
||||
|
||||
interface TrackAndFollowProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<div className="dialog-header">
|
||||
<span>Track & Follow</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
|
||||
const [trackedCharacters, setTrackedCharacters] = useState<string[]>([]);
|
||||
const [followedCharacter, setFollowedCharacter] = useState<string | null>(null);
|
||||
const { outCommand, data } = useMapRootState();
|
||||
const { trackingCharactersData } = data;
|
||||
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || characters.length === 0) {
|
||||
return;
|
||||
}
|
||||
const tracked = characters.filter(char => char.tracked).map(char => char.id);
|
||||
setTrackedCharacters(tracked);
|
||||
|
||||
const followed = characters.find(char => char.followed);
|
||||
setFollowedCharacter(followed ? followed.id : null);
|
||||
}, [visible, characters]);
|
||||
|
||||
const handleTrackToggle = (characterId: string) => {
|
||||
setTrackedCharacters(prev => {
|
||||
if (!prev.includes(characterId)) {
|
||||
return [...prev, characterId];
|
||||
}
|
||||
if (followedCharacter === characterId) {
|
||||
setFollowedCharacter(null);
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
}
|
||||
return prev.filter(id => id !== characterId);
|
||||
});
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
};
|
||||
|
||||
const handleFollowToggle = (characterId: string) => {
|
||||
if (followedCharacter !== characterId && !trackedCharacters.includes(characterId)) {
|
||||
setTrackedCharacters(prev => [...prev, characterId]);
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
}
|
||||
setFollowedCharacter(prev => (prev === characterId ? null : characterId));
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
};
|
||||
|
||||
const rowTemplate = (character: TrackingCharacter) => {
|
||||
return (
|
||||
<TrackingCharacterWrapper
|
||||
key={character.id}
|
||||
character={character}
|
||||
isTracked={trackedCharacters.includes(character.id)}
|
||||
isFollowed={followedCharacter === character.id}
|
||||
onTrackToggle={() => handleTrackToggle(character.id)}
|
||||
onFollowToggle={() => handleFollowToggle(character.id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={renderHeader()}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
modal
|
||||
className={classes.trackFollowDialog}
|
||||
closeOnEscape
|
||||
showHeader={true}
|
||||
closable={true}
|
||||
>
|
||||
<div className={classes.characterGrid}>
|
||||
<div className={classes.characterGridHeader}>
|
||||
<div>Track</div>
|
||||
<div>Follow</div>
|
||||
<div>Character</div>
|
||||
</div>
|
||||
<VirtualScroller
|
||||
items={characters}
|
||||
itemSize={48}
|
||||
itemTemplate={rowTemplate}
|
||||
className={`${classes.characterGridBody} h-72 w-full`}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* Character grid row styles */
|
||||
.characterGridRow {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: 0.5rem 0;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gridCellTrack,
|
||||
.gridCellFollow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gridCellCharacter {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterDetails {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radioContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portraitImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
margin-left: 0.5rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
margin-left: 0.25rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 640px) {
|
||||
.characterGridRow {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { TrackingCharacter } from './types';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
|
||||
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
|
||||
import classes from './TrackingCharacterWrapper.module.scss';
|
||||
|
||||
interface TrackingCharacterWrapperProps {
|
||||
character: TrackingCharacter;
|
||||
isTracked: boolean;
|
||||
isFollowed: boolean;
|
||||
onTrackToggle: () => void;
|
||||
onFollowToggle: () => void;
|
||||
}
|
||||
|
||||
export const TrackingCharacterWrapper = ({
|
||||
character,
|
||||
isTracked,
|
||||
isFollowed,
|
||||
onTrackToggle,
|
||||
onFollowToggle,
|
||||
}: TrackingCharacterWrapperProps) => {
|
||||
const trackCheckboxId = `track-${character.id}`;
|
||||
const followRadioId = `follow-${character.id}`;
|
||||
|
||||
return (
|
||||
<div className={classes.characterGridRow}>
|
||||
<div className={classes.gridCellTrack}>
|
||||
<Tooltip target={`#${trackCheckboxId}`} content="Track this character on the map" position="top" />
|
||||
<div className={classes.checkboxContainer}>
|
||||
<WdCheckbox label="" value={isTracked} onChange={() => onTrackToggle()} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.gridCellFollow}>
|
||||
<Tooltip target={`#${followRadioId}`} content="Follow this character's movements on the map" position="top" />
|
||||
<div className={classes.radioContainer}>
|
||||
<WdRadioButton
|
||||
id={followRadioId}
|
||||
name="followed_character"
|
||||
checked={isFollowed}
|
||||
onChange={() => onFollowToggle()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.gridCellCharacter}>
|
||||
<div className={classes.characterInfo}>
|
||||
<div className={classes.characterPortrait}>
|
||||
<img src={character.portrait_url} alt={character.name} className={classes.portraitImage} />
|
||||
</div>
|
||||
<div className={classes.characterDetails}>
|
||||
<span className={classes.characterName}>{character.name}</span>
|
||||
<span className={classes.corporationTicker}>[{character.corporation_ticker}]</span>
|
||||
{character.alliance_ticker && <span className={classes.allianceTicker}>[{character.alliance_ticker}]</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Interface for a character that can be tracked and followed
|
||||
*/
|
||||
export interface TrackingCharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
corporation_ticker: string;
|
||||
alliance_ticker?: string;
|
||||
portrait_url: string;
|
||||
tracked: boolean;
|
||||
followed: boolean;
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import type { ActivitySummary } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
|
||||
/**
|
||||
* Hook for character activity related handlers
|
||||
*/
|
||||
export const useCharacterActivityHandlers = () => {
|
||||
const { outCommand, update } = useMapRootState();
|
||||
|
||||
/**
|
||||
* Handle hiding the character activity dialog
|
||||
*/
|
||||
const handleHideCharacterActivity = useCallback(() => {
|
||||
// Update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showCharacterActivity: false,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.hideActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle showing the character activity dialog
|
||||
*/
|
||||
const handleShowActivity = useCallback(() => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle updating character activity data
|
||||
*/
|
||||
const handleUpdateActivity = useCallback(
|
||||
(activityData: { activity: ActivitySummary[] }) => {
|
||||
if (!activityData || !activityData.activity) {
|
||||
console.error('Invalid activity data received:', activityData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state with the activity data
|
||||
update(state => ({
|
||||
...state,
|
||||
characterActivityData: activityData.activity,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
return {
|
||||
handleHideCharacterActivity,
|
||||
handleShowActivity,
|
||||
handleUpdateActivity,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, CommandData, Commands } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import type { TrackingCharacter } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
/**
|
||||
* Hook for track and follow related handlers
|
||||
*/
|
||||
export const useTrackAndFollowHandlers = () => {
|
||||
const { outCommand, update } = useMapRootState();
|
||||
|
||||
/**
|
||||
* Handle hiding the track and follow dialog
|
||||
*/
|
||||
const handleHideTracking = useCallback(() => {
|
||||
// Update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.hideTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle showing the track and follow dialog
|
||||
*/
|
||||
const handleShowTracking = useCallback(() => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle updating tracking data
|
||||
*/
|
||||
const handleUpdateTracking = useCallback(
|
||||
(trackingData: { characters: TrackingCharacter[] }) => {
|
||||
if (!trackingData || !trackingData.characters) {
|
||||
console.error('Invalid tracking data received:', trackingData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state with the tracking data
|
||||
update(state => ({
|
||||
...state,
|
||||
trackingCharactersData: trackingData.characters,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character tracking
|
||||
*/
|
||||
const handleToggleTrack = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle track command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character following
|
||||
*/
|
||||
const handleToggleFollow = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle follow command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Handle user settings updates
|
||||
*/
|
||||
const handleUserSettingsUpdated = useCallback((settingsData: CommandData[Commands.userSettingsUpdated]) => {
|
||||
if (!settingsData || !settingsData.settings) {
|
||||
console.error('Invalid settings data received:', settingsData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleHideTracking,
|
||||
handleShowTracking,
|
||||
handleUpdateTracking,
|
||||
handleToggleTrack,
|
||||
handleToggleFollow,
|
||||
handleUserSettingsUpdated,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
.RadioInput {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--surface-border, #ccc);
|
||||
border-radius: 50%;
|
||||
background-color: var(--surface-card, #fff);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #3B82F6);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
border-color: var(--primary-color, #3B82F6);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
background-color: var(--primary-color, #3B82F6);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color-dark, #2563EB);
|
||||
|
||||
&::after {
|
||||
background-color: var(--primary-color-dark, #2563EB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.2rem var(--primary-color-light, rgba(59, 130, 246, 0.25));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
border-color: var(--surface-border, #ccc);
|
||||
|
||||
&:checked::after {
|
||||
background-color: var(--surface-border, #ccc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import styles from './WdRadioButton.module.scss';
|
||||
|
||||
export interface WdRadioButtonProps {
|
||||
id: string;
|
||||
name: string;
|
||||
checked: boolean;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WdRadioButton: React.FC<WdRadioButtonProps> = ({
|
||||
id,
|
||||
name,
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
className,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className={clsx('flex items-center', className)}>
|
||||
<input
|
||||
id={id}
|
||||
type="radio"
|
||||
name={name}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
styles.RadioInput,
|
||||
'w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600',
|
||||
)}
|
||||
/>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300 cursor-pointer"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WdRadioButton;
|
||||
@@ -0,0 +1,4 @@
|
||||
import WdRadioButton from './WdRadioButton';
|
||||
|
||||
export default WdRadioButton;
|
||||
export type { WdRadioButtonProps } from './WdRadioButton';
|
||||
@@ -13,3 +13,4 @@ export * from './WdCheckbox';
|
||||
export * from './TimeAgo';
|
||||
export * from './WdTooltipWrapper';
|
||||
export * from './WdResponsiveCheckBox';
|
||||
export * from './WdRadioButton';
|
||||
|
||||
@@ -16,12 +16,21 @@ import {
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DetailedKill } from '../types/kills';
|
||||
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
export type MapRootData = MapUnionTypes & {
|
||||
selectedSystems: string[];
|
||||
selectedConnections: Pick<SolarSystemConnection, 'source' | 'target'>[];
|
||||
linkSignatureToSystem: CommandLinkSignatureToSystem | null;
|
||||
detailedKills: Record<string, DetailedKill[]>;
|
||||
showCharacterActivity: boolean;
|
||||
characterActivityData: {
|
||||
activity: ActivitySummary[];
|
||||
loading?: boolean;
|
||||
};
|
||||
showTrackAndFollow: boolean;
|
||||
trackingCharactersData: TrackingCharacter[];
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -29,6 +38,13 @@ const INITIAL_DATA: MapRootData = {
|
||||
wormholes: [],
|
||||
effects: {},
|
||||
characters: [],
|
||||
showCharacterActivity: false,
|
||||
characterActivityData: {
|
||||
activity: [],
|
||||
loading: false
|
||||
},
|
||||
showTrackAndFollow: false,
|
||||
trackingCharactersData: [],
|
||||
userCharacters: [],
|
||||
presentCharacters: [],
|
||||
systems: [],
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './useRoutes';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCommandsSystems';
|
||||
export * from './useCommandsCharacters';
|
||||
export * from './useCommandsActivity';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandCharacterActivityData,
|
||||
CommandTrackingCharactersData,
|
||||
CommandUserSettingsUpdated,
|
||||
Commands,
|
||||
} from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { MapRootData } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
export const useCommandsActivity = () => {
|
||||
const { update } = useMapRootState();
|
||||
|
||||
const ref = useRef({ update });
|
||||
ref.current = { update };
|
||||
|
||||
const characterActivityData = useCallback((data: CommandCharacterActivityData) => {
|
||||
try {
|
||||
ref.current.update((state: MapRootData) => ({
|
||||
...state,
|
||||
characterActivityData: {
|
||||
activity: data.activity,
|
||||
loading: data.loading,
|
||||
},
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to process character activity data:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const trackingCharactersData = useCallback((data: CommandTrackingCharactersData) => {
|
||||
ref.current.update((state: MapRootData) => ({
|
||||
...state,
|
||||
trackingCharactersData: data.characters,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const hideActivity = useCallback(() => {
|
||||
ref.current.update((state: MapRootData) => ({
|
||||
...state,
|
||||
showCharacterActivity: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const hideTracking = useCallback(() => {
|
||||
ref.current.update((state: MapRootData) => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const userSettingsUpdated = useCallback((data: CommandUserSettingsUpdated) => {
|
||||
emitMapEvent({ name: Commands.userSettingsUpdated, data });
|
||||
}, []);
|
||||
|
||||
return { characterActivityData, trackingCharactersData, userSettingsUpdated, hideActivity, hideTracking };
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
CommandCharacterActivityData,
|
||||
CommandCharacterAdded,
|
||||
CommandCharacterRemoved,
|
||||
CommandCharactersUpdated,
|
||||
@@ -13,10 +14,12 @@ import {
|
||||
CommandRemoveConnections,
|
||||
CommandRemoveSystems,
|
||||
CommandRoutes,
|
||||
Commands,
|
||||
CommandSignaturesUpdated,
|
||||
CommandTrackingCharactersData,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
CommandUserSettingsUpdated,
|
||||
Commands,
|
||||
MapHandlers,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
|
||||
@@ -29,6 +32,7 @@ import {
|
||||
useRoutes,
|
||||
} from './api';
|
||||
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
|
||||
@@ -47,6 +51,7 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
useCommandsCharacters();
|
||||
const mapUpdated = useMapUpdated();
|
||||
const mapRoutes = useRoutes();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
@@ -123,6 +128,24 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -4,7 +4,9 @@ import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { SignatureGroup, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
export enum Commands {
|
||||
init = 'init',
|
||||
@@ -27,6 +29,11 @@ export enum Commands {
|
||||
selectSystem = 'select_system',
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
signaturesUpdated = 'signatures_updated',
|
||||
characterActivityData = 'character_activity_data',
|
||||
trackingCharactersData = 'tracking_characters_data',
|
||||
updateActivity = 'update_activity',
|
||||
updateTracking = 'update_tracking',
|
||||
userSettingsUpdated = 'user_settings_updated',
|
||||
}
|
||||
|
||||
export type Command =
|
||||
@@ -49,7 +56,12 @@ export type Command =
|
||||
| Commands.selectSystem
|
||||
| Commands.centerSystem
|
||||
| Commands.linkSignatureToSystem
|
||||
| Commands.signaturesUpdated;
|
||||
| Commands.signaturesUpdated
|
||||
| Commands.characterActivityData
|
||||
| Commands.trackingCharactersData
|
||||
| Commands.userSettingsUpdated
|
||||
| Commands.updateActivity
|
||||
| Commands.updateTracking;
|
||||
|
||||
export type CommandInit = {
|
||||
systems: SolarSystemRawType[];
|
||||
@@ -68,6 +80,7 @@ export type CommandInit = {
|
||||
reset?: boolean;
|
||||
is_subscription_active?: boolean;
|
||||
};
|
||||
|
||||
export type CommandAddSystems = SolarSystemRawType[];
|
||||
export type CommandUpdateSystems = SolarSystemRawType[];
|
||||
export type CommandRemoveSystems = number[];
|
||||
@@ -91,6 +104,46 @@ export type CommandLinkSignatureToSystem = {
|
||||
solar_system_target: number;
|
||||
};
|
||||
export type CommandLinkSignaturesUpdated = number;
|
||||
export type CommandCharacterActivityData = { activity: ActivitySummary[]; loading?: boolean };
|
||||
export type CommandTrackingCharactersData = { characters: TrackingCharacter[] };
|
||||
export type CommandUserSettingsUpdated = {
|
||||
settings: UserSettings;
|
||||
};
|
||||
|
||||
export type CommandShowActivity = null;
|
||||
export type CommandHideActivity = null;
|
||||
export type CommandShowTracking = null;
|
||||
export type CommandHideTracking = null;
|
||||
export type CommandUiLoaded = { version: string | null };
|
||||
export type CommandLogMapError = { error: string; componentStack: string };
|
||||
export type CommandMapEvent = { type: Command; data: unknown };
|
||||
export type CommandMapEvents = Array<{ type: Command; data: unknown }>;
|
||||
export type CommandUpdateActivity = {
|
||||
characterId: number;
|
||||
systemId: number;
|
||||
shipTypeId: number;
|
||||
timestamp: number;
|
||||
};
|
||||
export type CommandUpdateTracking = {
|
||||
characterId: number;
|
||||
track: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
|
||||
export interface UserSettings {
|
||||
primaryCharacterId?: string;
|
||||
mapSettings?: {
|
||||
showGrid?: boolean;
|
||||
snapToGrid?: boolean;
|
||||
gridSize?: number;
|
||||
};
|
||||
interfaceSettings?: {
|
||||
theme?: string;
|
||||
showMinimap?: boolean;
|
||||
showMenu?: boolean;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CommandData {
|
||||
[Commands.init]: CommandInit;
|
||||
@@ -113,6 +166,11 @@ export interface CommandData {
|
||||
[Commands.centerSystem]: CommandCenterSystem;
|
||||
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
|
||||
[Commands.signaturesUpdated]: CommandLinkSignaturesUpdated;
|
||||
[Commands.characterActivityData]: CommandCharacterActivityData;
|
||||
[Commands.trackingCharactersData]: CommandTrackingCharactersData;
|
||||
[Commands.userSettingsUpdated]: CommandUserSettingsUpdated;
|
||||
[Commands.updateActivity]: CommandUpdateActivity;
|
||||
[Commands.updateTracking]: CommandUpdateTracking;
|
||||
}
|
||||
|
||||
export interface MapHandlers {
|
||||
@@ -159,10 +217,13 @@ export enum OutCommand {
|
||||
getCorporationTicker = 'get_corporation_ticker',
|
||||
getSystemKills = 'get_system_kills',
|
||||
getSystemsKills = 'get_systems_kills',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
|
||||
hideActivity = 'hide_activity',
|
||||
showActivity = 'show_activity',
|
||||
hideTracking = 'hide_tracking',
|
||||
showTracking = 'show_tracking',
|
||||
toggleTrack = 'toggle_track',
|
||||
toggleFollow = 'toggle_follow',
|
||||
getUserSettings = 'get_user_settings',
|
||||
updateUserSettings = 'update_user_settings',
|
||||
unlinkSignature = 'unlink_signature',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RefObject, useCallback } from 'react';
|
||||
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
|
||||
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ defmodule WandererApp.Map do
|
||||
it
|
||||
"""
|
||||
require Logger
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
alias WandererApp.Utils.CharacterUtil
|
||||
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
@@ -521,4 +523,93 @@ defmodule WandererApp.Map do
|
||||
|
||||
defp _maybe_limit_list(list, nil), do: list
|
||||
defp _maybe_limit_list(list, limit), do: Enum.take(list, limit)
|
||||
|
||||
@doc """
|
||||
Gets raw character activity data for a map.
|
||||
Returns the raw activity data that can be processed by CharacterUtil.
|
||||
Only includes characters that are on the map's ACL.
|
||||
"""
|
||||
def get_character_activity(map_id) do
|
||||
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
|
||||
map = Ash.load!(map, :acls)
|
||||
|
||||
{:ok, jumps} = WandererApp.Api.MapChainPassages.by_map_id(%{map_id: map_id})
|
||||
thirty_days_ago = DateTime.utc_now() |> DateTime.add(-30 * 24 * 3600, :second)
|
||||
|
||||
# Get activity data
|
||||
connections_activity = get_connections_activity(map_id, thirty_days_ago)
|
||||
signatures_activity = get_signatures_activity(map_id, thirty_days_ago)
|
||||
|
||||
# Return raw activity data
|
||||
jumps
|
||||
|> Enum.map(fn passage ->
|
||||
%{
|
||||
character_name: passage.character.name,
|
||||
corporation_ticker: passage.character.corporation_ticker,
|
||||
alliance_ticker: passage.character.alliance_ticker || "",
|
||||
portrait_url: EVEUtil.get_portrait_url(passage.character.eve_id, 64),
|
||||
passages: passage.count,
|
||||
connections: Map.get(connections_activity, passage.character.id, 0),
|
||||
signatures: Map.get(signatures_activity, passage.character.id, 0),
|
||||
timestamp: DateTime.utc_now(),
|
||||
character_eve_id: passage.character.eve_id,
|
||||
character_id: passage.character.id,
|
||||
user_id: passage.character.user_id
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_connections_activity(map_id, thirty_days_ago) do
|
||||
import Ecto.Query
|
||||
|
||||
from(ua in WandererApp.Api.UserActivity,
|
||||
join: c in assoc(ua, :character),
|
||||
where:
|
||||
ua.entity_id == ^map_id and
|
||||
ua.entity_type == :map and
|
||||
ua.event_type == :map_connection_added and
|
||||
ua.inserted_at > ^thirty_days_ago,
|
||||
group_by: [c.id],
|
||||
select: {c.id, count(ua.id)}
|
||||
)
|
||||
|> WandererApp.Repo.all()
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp get_signatures_activity(map_id, thirty_days_ago) do
|
||||
import Ecto.Query
|
||||
|
||||
from(ua in WandererApp.Api.UserActivity,
|
||||
join: c in assoc(ua, :character),
|
||||
where:
|
||||
ua.entity_id == ^map_id and
|
||||
ua.entity_type == :map and
|
||||
ua.event_type == :signatures_added and
|
||||
ua.inserted_at > ^thirty_days_ago,
|
||||
select: {ua.character_id, ua.event_data}
|
||||
)
|
||||
|> WandererApp.Repo.all()
|
||||
|> process_signatures_data()
|
||||
end
|
||||
|
||||
defp process_signatures_data(signatures_data) do
|
||||
signatures_data
|
||||
|> Enum.group_by(fn {character_id, _} -> character_id end)
|
||||
|> Enum.map(&process_character_signatures/1)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp process_character_signatures({character_id, activities}) do
|
||||
signature_count =
|
||||
activities
|
||||
|> Enum.map(fn {_, event_data} ->
|
||||
case Jason.decode(event_data) do
|
||||
{:ok, data} -> length(Map.get(data, "signatures", []))
|
||||
_ -> 0
|
||||
end
|
||||
end)
|
||||
|> Enum.sum()
|
||||
|
||||
{character_id, signature_count}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -350,6 +350,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
Impl.broadcast!(map_id, :add_connection, connection)
|
||||
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
{:ok, character_with_user} = character |> Ash.load(:user)
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
|
||||
character_id: character_id,
|
||||
user_id: character_with_user.user_id,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: old_location.solar_system_id,
|
||||
solar_system_target_id: location.solar_system_id
|
||||
})
|
||||
|
||||
Impl.broadcast!(map_id, :maybe_link_signature, %{
|
||||
character_id: character_id,
|
||||
solar_system_source: old_location.solar_system_id,
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule WandererApp.Maps do
|
||||
use Nebulex.Caching
|
||||
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
@minimum_route_attrs [
|
||||
:system_class,
|
||||
@@ -170,13 +171,21 @@ defmodule WandererApp.Maps do
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}} = _get_map_characters(map)
|
||||
|
||||
user_characters
|
||||
filtered_characters = user_characters
|
||||
|> Enum.filter(fn c ->
|
||||
c.id == map.owner_id or
|
||||
c.id in map_acl_owner_ids or c.eve_id in map_member_eve_ids or
|
||||
to_string(c.corporation_id) in map_member_corporation_ids or
|
||||
to_string(c.alliance_id) in map_member_alliance_ids
|
||||
is_owner = c.id == map.owner_id
|
||||
is_acl_owner = c.id in map_acl_owner_ids
|
||||
is_member_eve = c.eve_id in map_member_eve_ids
|
||||
is_member_corp = to_string(c.corporation_id) in map_member_corporation_ids
|
||||
is_member_alliance = to_string(c.alliance_id) in map_member_alliance_ids
|
||||
|
||||
has_access = is_owner or is_acl_owner or is_member_eve or is_member_corp or is_member_alliance
|
||||
|
||||
has_access
|
||||
end)
|
||||
|
||||
|
||||
filtered_characters
|
||||
end
|
||||
|
||||
defp filter_blocked_maps(maps, current_user) do
|
||||
|
||||
@@ -4,7 +4,8 @@ defmodule WandererApp.MapUserSettingsRepo do
|
||||
@default_form_data %{
|
||||
"select_on_spash" => false,
|
||||
"link_signature_on_splash" => false,
|
||||
"delete_connection_with_sigs" => false
|
||||
"delete_connection_with_sigs" => false,
|
||||
"primary_character_id" => nil
|
||||
}
|
||||
|
||||
def get(map_id, user_id) do
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
defmodule WandererApp.Utils.CharacterUtil do
|
||||
@moduledoc """
|
||||
Utility functions for character-related operations.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Finds a followed character ID from a list of character settings and activities.
|
||||
|
||||
## Parameters
|
||||
- `character_settings`: List of character settings with `followed` and `character_id` fields
|
||||
- `activities_by_character`: Map of activities grouped by character_id
|
||||
- `is_current_user`: Boolean indicating if this is for the current user
|
||||
|
||||
## Returns
|
||||
- Character ID of the followed character if found, nil otherwise
|
||||
"""
|
||||
def find_followed_character(character_settings, activities_by_character, is_current_user) do
|
||||
if is_current_user do
|
||||
followed_chars =
|
||||
character_settings
|
||||
|> Enum.filter(& &1.followed)
|
||||
|> Enum.map(& &1.character_id)
|
||||
|
||||
# Find if any of user's characters is followed
|
||||
user_char_ids = Map.keys(activities_by_character)
|
||||
|
||||
Enum.find(followed_chars, fn followed_id ->
|
||||
followed_id in user_char_ids
|
||||
end)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Finds the character with the most activity from a map of activities grouped by character_id.
|
||||
|
||||
## Parameters
|
||||
- `activities_by_character`: Map of activities grouped by character_id
|
||||
|
||||
## Returns
|
||||
- Character ID of the character with the most activity, or nil if no activities
|
||||
"""
|
||||
def find_most_active_character(activities_by_character) do
|
||||
if Enum.empty?(activities_by_character) do
|
||||
nil
|
||||
else
|
||||
{char_id, _} =
|
||||
activities_by_character
|
||||
|> Enum.map(fn {char_id, activities} ->
|
||||
total_activity =
|
||||
activities
|
||||
|> Enum.map(fn a ->
|
||||
Map.get(a, :passages, 0) +
|
||||
Map.get(a, :connections, 0) +
|
||||
Map.get(a, :signatures, 0)
|
||||
end)
|
||||
|> Enum.sum()
|
||||
|
||||
{char_id, total_activity}
|
||||
end)
|
||||
|> Enum.max_by(fn {_, count} -> count end, fn -> {nil, 0} end)
|
||||
|
||||
char_id
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Processes character activity data for display.
|
||||
|
||||
## Parameters
|
||||
- `map_id`: ID of the map
|
||||
- `current_user`: Current user struct
|
||||
|
||||
## Returns
|
||||
- List of processed activity data
|
||||
"""
|
||||
def process_character_activity(map_id, current_user) do
|
||||
with {:ok, character_settings} <- get_map_character_settings(map_id),
|
||||
raw_activity <- WandererApp.Map.get_character_activity(map_id),
|
||||
{:ok, user_characters} <-
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
|
||||
process_activity_data(raw_activity, character_settings, user_characters, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_character_settings(map_id) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_activity_data([], _character_settings, _user_characters, _current_user), do: []
|
||||
defp process_activity_data([%{is_user: _} | _] = activity_data, _, _, _), do: activity_data
|
||||
|
||||
defp process_activity_data(all_activity, character_settings, user_characters, current_user) do
|
||||
all_activity
|
||||
|> group_by_user_id()
|
||||
|> process_users_activity(character_settings, user_characters, current_user)
|
||||
|> sort_by_timestamp()
|
||||
|> group_and_select_most_active()
|
||||
end
|
||||
|
||||
defp group_by_user_id(activities) do
|
||||
Enum.group_by(activities, &Map.get(&1, :user_id, "unknown"))
|
||||
end
|
||||
|
||||
defp process_users_activity(
|
||||
activity_by_user_id,
|
||||
character_settings,
|
||||
user_characters,
|
||||
current_user
|
||||
) do
|
||||
Enum.flat_map(activity_by_user_id, fn {user_id, user_activities} ->
|
||||
process_single_user_activity(
|
||||
user_id,
|
||||
user_activities,
|
||||
character_settings,
|
||||
user_characters,
|
||||
current_user
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_single_user_activity(
|
||||
user_id,
|
||||
user_activities,
|
||||
character_settings,
|
||||
user_characters,
|
||||
current_user
|
||||
) do
|
||||
is_current_user = user_id == current_user.id
|
||||
activities_by_character = group_activities_by_character(user_activities)
|
||||
|
||||
char_id_to_show =
|
||||
select_character_to_show(activities_by_character, character_settings, is_current_user)
|
||||
|
||||
case char_id_to_show do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
id ->
|
||||
create_character_activity_entry(
|
||||
id,
|
||||
activities_by_character,
|
||||
user_characters,
|
||||
current_user,
|
||||
is_current_user,
|
||||
user_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_activities_by_character(activities) do
|
||||
Enum.group_by(activities, fn activity ->
|
||||
Map.get(activity, :character_id) || Map.get(activity, :character_eve_id)
|
||||
end)
|
||||
end
|
||||
|
||||
defp select_character_to_show(activities_by_character, character_settings, is_current_user) do
|
||||
followed_char_id =
|
||||
find_followed_character(character_settings, activities_by_character, is_current_user)
|
||||
|
||||
followed_char_id || find_most_active_character(activities_by_character)
|
||||
end
|
||||
|
||||
defp create_character_activity_entry(
|
||||
char_id,
|
||||
activities_by_character,
|
||||
user_characters,
|
||||
current_user,
|
||||
is_current_user,
|
||||
user_id
|
||||
) do
|
||||
char_activities = Map.get(activities_by_character, char_id, [])
|
||||
|
||||
with char_details when not is_nil(char_details) <-
|
||||
get_character_details(char_id, char_activities, user_characters, is_current_user) do
|
||||
[
|
||||
build_activity_entry(
|
||||
char_details,
|
||||
char_activities,
|
||||
current_user,
|
||||
is_current_user,
|
||||
user_id
|
||||
)
|
||||
]
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp get_character_details(char_id, char_activities, user_characters, true) do
|
||||
Enum.find(user_characters, fn char ->
|
||||
char.id == char_id || to_string(char.eve_id) == char_id
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_character_details(char_id, [activity | _], _user_characters, false) do
|
||||
%{
|
||||
id: char_id,
|
||||
name: Map.get(activity, :character_name, "Unknown"),
|
||||
eve_id: Map.get(activity, :character_eve_id, nil),
|
||||
corporation_ticker: Map.get(activity, :corporation_ticker, ""),
|
||||
alliance_ticker: Map.get(activity, :alliance_ticker, "")
|
||||
}
|
||||
end
|
||||
|
||||
defp build_activity_entry(char_details, char_activities, current_user, is_current_user, user_id) do
|
||||
%{
|
||||
character_id: char_details.eve_id || char_details.id,
|
||||
character_name: char_details.name,
|
||||
portrait_url: WandererApp.Utils.EVEUtil.get_portrait_url(char_details.eve_id, 64),
|
||||
corporation_ticker: char_details.corporation_ticker,
|
||||
alliance_ticker: Map.get(char_details, :alliance_ticker, ""),
|
||||
passages: sum_activity(char_activities, :passages),
|
||||
connections: sum_activity(char_activities, :connections),
|
||||
signatures: sum_activity(char_activities, :signatures),
|
||||
timestamp: get_most_recent_timestamp(char_activities),
|
||||
user_name: if(is_current_user, do: current_user.name, else: char_details.name)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_system_info(activities, key, default),
|
||||
do: Map.get(List.first(activities) || %{}, key, default)
|
||||
|
||||
defp sum_activity(activities, key),
|
||||
do: activities |> Enum.map(&Map.get(&1, key, 0)) |> Enum.sum()
|
||||
|
||||
defp get_most_recent_timestamp(activities) do
|
||||
activities
|
||||
|> Enum.map(&Map.get(&1, :timestamp, DateTime.utc_now()))
|
||||
|> Enum.sort_by(& &1, {:desc, DateTime})
|
||||
|> List.first() || DateTime.utc_now()
|
||||
end
|
||||
|
||||
defp sort_by_timestamp(activities) do
|
||||
Enum.sort_by(activities, & &1.timestamp, {:desc, DateTime})
|
||||
end
|
||||
|
||||
defp group_and_select_most_active(activities) do
|
||||
activities
|
||||
|> Enum.group_by(&Map.get(&1, :user_id, "unknown"))
|
||||
|> Enum.map(fn {_user_id, user_activities} ->
|
||||
user_activities
|
||||
|> Enum.sort_by(
|
||||
fn activity ->
|
||||
Map.get(activity, :passages, 0) +
|
||||
Map.get(activity, :connections, 0) +
|
||||
Map.get(activity, :signatures, 0)
|
||||
end,
|
||||
:desc
|
||||
)
|
||||
|> List.first()
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
defmodule WandererApp.Utils.EVEUtil do
|
||||
@moduledoc """
|
||||
Utility functions for EVE Online related operations.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a URL for a character portrait.
|
||||
|
||||
## Parameters
|
||||
* `eve_id` - The EVE Online character ID
|
||||
* `size` - The size of the portrait (default: 64)
|
||||
|
||||
## Examples
|
||||
iex> WandererApp.Utils.EVEUtil.get_portrait_url(12345678)
|
||||
"https://images.evetech.net/characters/12345678/portrait?size=64"
|
||||
|
||||
iex> WandererApp.Utils.EVEUtil.get_portrait_url(12345678, 128)
|
||||
"https://images.evetech.net/characters/12345678/portrait?size=128"
|
||||
"""
|
||||
def get_portrait_url(eve_id, size \\ 64)
|
||||
def get_portrait_url(nil, size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
|
||||
def get_portrait_url("", size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
|
||||
def get_portrait_url(eve_id, size) do
|
||||
"https://images.evetech.net/characters/#{eve_id}/portrait?size=#{size}"
|
||||
end
|
||||
end
|
||||
@@ -17,6 +17,7 @@ defmodule WandererAppWeb.CharacterActivity do
|
||||
|> handle_info_or_assign(assigns)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
# attr(:groups, :any, required: true)
|
||||
# attr(:character_settings, :any, required: true)
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
@@ -66,7 +67,7 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("undo", %{"event-data" => event_data} = _params, socket) do
|
||||
def handle_event("undo", %{"event-data" => _event_data} = _params, socket) do
|
||||
# notify_to(socket.assigns.notify_to, socket.assigns.event_name, map_slug)
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -78,7 +79,4 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_event_name(name), do: name
|
||||
|
||||
defp get_event_data(_name, data), do: Jason.encode!(data)
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
# attr(:stream, :any, required: true)
|
||||
# attr(:page, :integer, required: true)
|
||||
# attr(:end_of_stream?, :boolean, required: true)
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
@@ -123,7 +123,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("undo", %{"event-data" => event_data} = _params, socket) do
|
||||
def handle_event("undo", %{"event-data" => _event_data} = _params, socket) do
|
||||
# notify_to(socket.assigns.notify_to, socket.assigns.event_name, map_slug)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
@@ -351,7 +351,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing required field: owner_eve_id"})
|
||||
|
||||
{:error, "owner_eve_id does not match any existing character"} = error ->
|
||||
{:error, "owner_eve_id does not match any existing character"} = _error ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Character not found: The provided owner_eve_id does not match any existing character"})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
defmodule WandererAppWeb.MapActivityEventHandler do
|
||||
@moduledoc """
|
||||
Handles map activity events and updates for the live view.
|
||||
"""
|
||||
use WandererAppWeb, :live_component
|
||||
use Phoenix.Component
|
||||
require Logger
|
||||
@@ -7,40 +10,47 @@ defmodule WandererAppWeb.MapActivityEventHandler do
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: :character_activity,
|
||||
payload: character_activity
|
||||
},
|
||||
event: event,
|
||||
payload: {:activity_data, activity_data}
|
||||
} = _full_event,
|
||||
socket
|
||||
),
|
||||
do: socket |> assign(:character_activity, character_activity)
|
||||
)
|
||||
when event in [:character_activity, :character_activity_data] do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"character_activity_data",
|
||||
%{
|
||||
activity: activity_data,
|
||||
loading: false
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# Fallback for non-tagged activity data (for backward compatibility)
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: event,
|
||||
payload: activity_data
|
||||
} = _full_event,
|
||||
socket
|
||||
)
|
||||
when event in [:character_activity, :character_activity_data] do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"character_activity_data",
|
||||
%{
|
||||
activity: activity_data,
|
||||
loading: false
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(event, socket),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
|
||||
def handle_ui_event("show_activity", _, %{assigns: %{map_id: map_id}} = socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:show_activity?, true)
|
||||
|> assign_async(:character_activity, fn ->
|
||||
map_id |> get_character_activity()
|
||||
end)}
|
||||
end
|
||||
|
||||
def handle_ui_event("hide_activity", _, socket),
|
||||
do: {:noreply, socket |> assign(show_activity?: false)}
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
defp get_character_activity(map_id) do
|
||||
{:ok, jumps} = WandererApp.Api.MapChainPassages.by_map_id(%{map_id: map_id})
|
||||
|
||||
jumps =
|
||||
jumps
|
||||
|> Enum.map(fn p ->
|
||||
%{p | character: p.character |> MapEventHandler.map_ui_character_stat()}
|
||||
end)
|
||||
|
||||
{:ok, %{character_activity: jumps}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
@moduledoc """
|
||||
Handles character-related events and UI interactions for the map live view.
|
||||
"""
|
||||
use WandererAppWeb, :live_component
|
||||
use Phoenix.Component
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
|
||||
def handle_server_event(%{event: :character_added, payload: character}, socket) do
|
||||
socket
|
||||
@@ -60,6 +64,36 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
present_character_eve_ids
|
||||
)
|
||||
|
||||
def handle_server_event(
|
||||
%{event: :tracking_characters_data, payload: tracking_data},
|
||||
socket
|
||||
) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(
|
||||
%{event: :character_activity_data, payload: {:activity_data, activity_data}},
|
||||
socket
|
||||
) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"character_activity_data",
|
||||
%{activity: activity_data, loading: false}
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(%{event: :character_activity, payload: activity_data}, socket) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"character_activity",
|
||||
activity_data
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(event, socket),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
|
||||
@@ -94,223 +128,211 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user,
|
||||
only_tracked_characters: only_tracked_characters
|
||||
only_tracked_characters: _only_tracked_characters
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
# Get all user characters
|
||||
{:ok, all_user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id})
|
||||
|
||||
socket =
|
||||
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
|
||||
nil ->
|
||||
{:ok, map_character_settings} =
|
||||
# Find the character that was clicked
|
||||
clicked_char =
|
||||
Enum.find(all_user_characters, fn char ->
|
||||
"#{char.id}" == "#{character_id}"
|
||||
end)
|
||||
|
||||
if clicked_char do
|
||||
# Get existing settings for this character on this map
|
||||
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id) do
|
||||
{:ok, existing_settings} ->
|
||||
# Toggle the tracked status
|
||||
if existing_settings.tracked do
|
||||
# Untrack the character
|
||||
{:ok, updated_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(existing_settings)
|
||||
|
||||
# If the character was also followed, unfollow it
|
||||
if updated_settings.followed do
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.unfollow(updated_settings)
|
||||
end
|
||||
else
|
||||
# Track the character
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Create new settings with tracked=true
|
||||
{:ok, _} =
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
character_id: clicked_char.id,
|
||||
map_id: map_id,
|
||||
tracked: true,
|
||||
followed: false
|
||||
})
|
||||
|
||||
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
|
||||
|
||||
:ok = track_characters([character], map_id, true)
|
||||
:ok = add_characters([character], map_id, true)
|
||||
|
||||
socket
|
||||
|
||||
character_setting ->
|
||||
case character_setting.tracked do
|
||||
true ->
|
||||
{:ok, map_character_settings} =
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.untrack()
|
||||
|
||||
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
|
||||
|
||||
:ok = untrack_characters([character], map_id)
|
||||
:ok = remove_characters([character], map_id)
|
||||
|
||||
if only_tracked_characters do
|
||||
Process.send_after(self(), :not_all_characters_tracked, 10)
|
||||
end
|
||||
|
||||
socket
|
||||
|
||||
_ ->
|
||||
{:ok, map_character_settings} =
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.track()
|
||||
|
||||
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
|
||||
|
||||
:ok = track_characters([character], map_id, true)
|
||||
:ok = add_characters([character], map_id, true)
|
||||
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
%{result: characters} = socket.assigns.characters
|
||||
# Get the map with ACLs
|
||||
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
|
||||
map = Ash.load!(map, :acls)
|
||||
|
||||
{:ok, map_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
# Get updated settings
|
||||
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
user_character_eve_ids = map_characters |> Enum.map(& &1.eve_id)
|
||||
# Get characters with access to the map
|
||||
{:ok, %{characters: characters_with_access}} =
|
||||
WandererApp.Maps.load_characters(map, updated_settings, current_user.id)
|
||||
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
# Build tracking data for all characters with map access
|
||||
tracking_data =
|
||||
Enum.map(characters_with_access, fn char ->
|
||||
# Find settings for this character if they exist
|
||||
setting = Enum.find(updated_settings, &(&1.character_id == char.id))
|
||||
tracked = if setting, do: setting.tracked, else: false
|
||||
followed = if setting, do: setting.followed, else: false
|
||||
|
||||
characters =
|
||||
characters
|
||||
|> Enum.map(fn c ->
|
||||
WandererApp.Maps.map_character(
|
||||
c,
|
||||
character_settings |> Enum.find(&(&1.character_id == c.id))
|
||||
)
|
||||
%{
|
||||
id: "#{char.id}",
|
||||
name: char.name,
|
||||
eve_id: char.eve_id,
|
||||
portrait_url: EVEUtil.get_portrait_url(char.eve_id),
|
||||
corporation_ticker: char.corporation_ticker,
|
||||
alliance_ticker: Map.get(char, :alliance_ticker, ""),
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
}
|
||||
end)
|
||||
|
||||
# Send the updated tracking data to the client
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"show_tracking",
|
||||
_,
|
||||
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
|
||||
) do
|
||||
# Get character settings for this map
|
||||
{:ok, character_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
# Get the map with ACLs
|
||||
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
|
||||
map = Ash.load!(map, :acls)
|
||||
|
||||
# Get all user characters
|
||||
{:ok, _all_user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id})
|
||||
|
||||
# Get characters that have access to the map using load_characters
|
||||
# This will include all characters with access, even if they're not tracked
|
||||
{:ok, %{characters: characters_with_access}} =
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id)
|
||||
|
||||
# Create tracking data for characters with access to the map
|
||||
tracking_data =
|
||||
Enum.map(characters_with_access, fn char ->
|
||||
# Find settings for this character if they exist
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
tracked = if setting, do: setting.tracked, else: false
|
||||
followed = if setting, do: setting.followed, else: false
|
||||
|
||||
%{
|
||||
id: char.id,
|
||||
name: char.name,
|
||||
corporation_ticker: char.corporation_ticker,
|
||||
alliance_ticker: Map.get(char, :alliance_ticker, ""),
|
||||
portrait_url: EVEUtil.get_portrait_url(char.eve_id),
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
}
|
||||
end)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(user_characters: user_character_eve_ids)
|
||||
|> assign(has_tracked_characters?: has_tracked_characters?(user_character_eve_ids))
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, %{characters: characters}}
|
||||
end)
|
||||
|> MapEventHandler.push_map_event(
|
||||
"init",
|
||||
%{
|
||||
user_characters: user_character_eve_ids,
|
||||
reset: false
|
||||
}
|
||||
)}
|
||||
event_data = %{
|
||||
type: "tracking_characters_data",
|
||||
body: %{characters: tracking_data}
|
||||
}
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:show_tracking, true)
|
||||
|> push_event("map_event", event_data)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"hide_tracking",
|
||||
_,
|
||||
socket
|
||||
) do
|
||||
{:noreply, socket |> assign(:show_tracking, false)}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"toggle_follow",
|
||||
%{"character-id" => clicked_char_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user
|
||||
}
|
||||
} = socket
|
||||
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
|
||||
) do
|
||||
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
# Find and filter user's characters
|
||||
{:ok, user_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
user_char_ids = Enum.map(user_characters, & &1.id)
|
||||
|
||||
my_settings =
|
||||
all_settings
|
||||
|> Enum.filter(fn s ->
|
||||
s.character_id in user_char_ids
|
||||
end)
|
||||
|
||||
existing = Enum.find(all_settings, &(&1.character_id == clicked_char_id))
|
||||
|
||||
{:ok, target_setting} =
|
||||
if not is_nil(existing) do
|
||||
{:ok, existing}
|
||||
else
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: clicked_char_id,
|
||||
map_id: map_id,
|
||||
tracked: true,
|
||||
followed: true
|
||||
})
|
||||
end
|
||||
|
||||
# If the target_setting is already followed => unfollow it
|
||||
if target_setting.followed do
|
||||
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
|
||||
with {:ok, clicked_char} <- find_user_character(current_user, clicked_char_id),
|
||||
{:ok, updated_settings} <- toggle_character_follow(map_id, clicked_char),
|
||||
{:ok, tracking_data} <- build_tracking_data(map_id, current_user) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
|
||||
else
|
||||
# Only unfollow other rows from the current user
|
||||
for s <- my_settings, s.id != target_setting.id, s.followed == true do
|
||||
WandererApp.MapCharacterSettingsRepo.unfollow!(s)
|
||||
end
|
||||
|
||||
# Ensure the new followed char is tracked
|
||||
if not target_setting.tracked do
|
||||
WandererApp.MapCharacterSettingsRepo.track!(target_setting)
|
||||
|
||||
char = target_setting |> Ash.load!(:character) |> Map.get(:character)
|
||||
:ok = track_characters([char], map_id, true)
|
||||
:ok = add_characters([char], map_id, true)
|
||||
end
|
||||
|
||||
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
|
||||
_ -> {:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
# re-fetch or re-map to confirm final results in UI
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
user_eve_ids = Enum.map(tracked_characters, & &1.eve_id)
|
||||
|
||||
{:ok, final_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
updated_chars =
|
||||
characters
|
||||
|> Enum.map(fn c ->
|
||||
s = Enum.find(final_settings, &(&1.character_id == c.id))
|
||||
WandererApp.Maps.map_character(c, s)
|
||||
end)
|
||||
|
||||
def handle_ui_event(
|
||||
"show_activity",
|
||||
_,
|
||||
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
|
||||
) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(user_characters: user_eve_ids)
|
||||
|> assign(has_tracked_characters?: has_tracked_characters?(user_eve_ids))
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, %{characters: updated_chars}}
|
||||
|> MapEventHandler.push_map_event(
|
||||
"character_activity_data",
|
||||
%{activity: [], loading: true}
|
||||
)
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
try do
|
||||
result =
|
||||
WandererApp.Utils.CharacterUtil.process_character_activity(map_id, current_user)
|
||||
|
||||
{:activity_data, result}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Error processing character activity: #{inspect(e)}")
|
||||
Logger.error("#{Exception.format_stacktrace()}")
|
||||
{:activity_data, []}
|
||||
end
|
||||
end)
|
||||
|> MapEventHandler.push_map_event("init", %{user_characters: user_eve_ids, reset: false})
|
||||
|
||||
{:noreply, socket |> assign(:character_activity_task, task)}
|
||||
end
|
||||
|
||||
def handle_ui_event("hide_activity", _, socket),
|
||||
do: {:noreply, socket |> assign(show_activity?: false)}
|
||||
|
||||
def handle_ui_event(event, params, socket) do
|
||||
Logger.debug(fn ->
|
||||
"unhandled event in MapCharactersEventHandler: #{inspect(event)} with params: #{inspect(params)}"
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event("hide_tracking", _, socket),
|
||||
do: {:noreply, socket |> assign(show_tracking?: false)}
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
def add_character(
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
map_id: map_id,
|
||||
user_permissions: %{track_character: true}
|
||||
}
|
||||
} = socket
|
||||
),
|
||||
do:
|
||||
socket
|
||||
|> assign(show_tracking?: true)
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, map} =
|
||||
map_id
|
||||
|> WandererApp.MapRepo.get([:acls])
|
||||
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
map
|
||||
|> WandererApp.Maps.load_characters(
|
||||
character_settings,
|
||||
current_user.id
|
||||
)
|
||||
end)
|
||||
|
||||
def add_character(socket), do: socket
|
||||
|
||||
def has_tracked_characters?([]), do: false
|
||||
@@ -327,9 +349,9 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker
|
||||
:alliance_name
|
||||
])
|
||||
|> Map.put(:alliance_ticker, Map.get(character, :alliance_ticker, ""))
|
||||
|> Map.put_new(:ship, WandererApp.Character.get_ship(character))
|
||||
|> Map.put_new(:location, get_location(character))
|
||||
|
||||
@@ -462,4 +484,289 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|
||||
defp get_location(character),
|
||||
do: %{solar_system_id: character.solar_system_id, structure_id: character.structure_id}
|
||||
|
||||
@doc """
|
||||
Initializes character tracking for a map. This is called when the map is first loaded.
|
||||
Returns a tuple with the socket and whether any characters need tracking setup.
|
||||
"""
|
||||
def init_character_tracking(socket, map_id, %{
|
||||
current_user: current_user,
|
||||
user_permissions: user_permissions
|
||||
}) do
|
||||
{:ok, character_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
{:ok, map} = get_map_with_acls(map_id)
|
||||
|
||||
{:ok, %{characters: characters_with_access}} =
|
||||
load_map_characters(map, character_settings, current_user)
|
||||
|
||||
socket = init_tracking_state(socket, current_user)
|
||||
|
||||
needs_tracking_setup =
|
||||
needs_tracking_setup?(characters_with_access, character_settings, user_permissions)
|
||||
|
||||
if needs_tracking_setup do
|
||||
show_tracking_dialog(socket, characters_with_access, character_settings)
|
||||
else
|
||||
{socket, needs_tracking_setup}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_with_acls(map_id) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, Ash.load!(map, :acls)}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_map_characters(map, character_settings, current_user) do
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id)
|
||||
end
|
||||
|
||||
defp init_tracking_state(socket, current_user) do
|
||||
user_character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
has_tracked_characters? = has_tracked_characters?(user_character_eve_ids)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
has_tracked_characters?: has_tracked_characters?,
|
||||
user_characters: user_character_eve_ids
|
||||
)
|
||||
end
|
||||
|
||||
defp needs_tracking_setup?(characters, character_settings, user_permissions) do
|
||||
untracked_count =
|
||||
characters
|
||||
|> Enum.count(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
not (setting && setting.tracked)
|
||||
end)
|
||||
|
||||
untracked_count > 0 && user_permissions.track_character
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles character tracking events during map initialization.
|
||||
"""
|
||||
def handle_tracking_events(socket, map_id, events) do
|
||||
events
|
||||
|> Enum.reduce(socket, fn event, socket ->
|
||||
handle_tracking_event(event, socket, map_id)
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_tracking_event({:track_characters, map_characters, track_character}, socket, map_id) do
|
||||
:ok = track_characters(map_characters, map_id, track_character)
|
||||
:ok = add_characters(map_characters, map_id, track_character)
|
||||
socket
|
||||
end
|
||||
|
||||
defp handle_tracking_event(:invalid_token_message, socket, _map_id) do
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"One of your characters has expired token. Please refresh it on characters page."
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_tracking_event(:map_character_limit, socket, _map_id) do
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Map reached its character limit, your characters won't be tracked. Please contact administrator."
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_tracking_event(:empty_tracked_characters, socket, _map_id), do: socket
|
||||
defp handle_tracking_event(_, socket, _map_id), do: socket
|
||||
|
||||
@doc """
|
||||
Gets character settings for a map.
|
||||
"""
|
||||
def get_map_character_settings(map_id) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a list of characters that need tracking setup.
|
||||
"""
|
||||
def get_untracked_characters(characters, character_settings) do
|
||||
Enum.filter(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
is_tracked = setting && setting.tracked
|
||||
!is_tracked
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows the tracking dialog with the given characters.
|
||||
"""
|
||||
def show_tracking_dialog(socket, characters_with_access, character_settings) do
|
||||
tracking_data =
|
||||
Enum.map(characters_with_access, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
tracked = if setting, do: setting.tracked, else: false
|
||||
followed = if setting, do: setting.followed, else: false
|
||||
|
||||
%{
|
||||
id: char.id,
|
||||
name: char.name,
|
||||
portrait_url: EVEUtil.get_portrait_url(char.eve_id, 64),
|
||||
corporation_ticker: char.corporation_ticker,
|
||||
alliance_ticker: Map.get(char, :alliance_ticker, ""),
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
}
|
||||
end)
|
||||
|
||||
socket
|
||||
|> push_event("map_event", %{
|
||||
type: "show_tracking",
|
||||
body: %{}
|
||||
})
|
||||
|> push_event("map_event", %{
|
||||
type: "tracking_characters_data",
|
||||
body: %{characters: tracking_data}
|
||||
})
|
||||
|> assign(:show_tracking, true)
|
||||
end
|
||||
|
||||
def handle_activity_data(socket, activity_data) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_activity_data", %{
|
||||
activity: activity_data,
|
||||
loading: false
|
||||
})
|
||||
end
|
||||
|
||||
def handle_tracking_result(socket, %{type: :character_tracking} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_tracking", result)
|
||||
end
|
||||
|
||||
def handle_settings_result(socket, %{type: :character_settings} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_settings", result)
|
||||
end
|
||||
|
||||
def handle_location_result(socket, %{type: :character_location} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_location", result)
|
||||
end
|
||||
|
||||
def handle_online_result(socket, %{type: :character_online} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_online", result)
|
||||
end
|
||||
|
||||
def handle_ship_result(socket, %{type: :character_ship} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_ship", result)
|
||||
end
|
||||
|
||||
def handle_fleet_result(socket, %{type: :character_fleet} = result) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("character_fleet", result)
|
||||
end
|
||||
|
||||
def handle_character_result(socket, type, result) do
|
||||
case type do
|
||||
:character_activity -> handle_activity_data(socket, result)
|
||||
:character_tracking -> handle_tracking_result(socket, result)
|
||||
:character_settings -> handle_settings_result(socket, result)
|
||||
:character_location -> handle_location_result(socket, result)
|
||||
:character_online -> handle_online_result(socket, result)
|
||||
:character_ship -> handle_ship_result(socket, result)
|
||||
:character_fleet -> handle_fleet_result(socket, result)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_user_character(current_user, char_id) do
|
||||
case Enum.find(current_user.characters, &("#{&1.id}" == "#{char_id}")) do
|
||||
nil -> {:error, :character_not_found}
|
||||
char -> {:ok, char}
|
||||
end
|
||||
end
|
||||
|
||||
defp toggle_character_follow(map_id, clicked_char) do
|
||||
with {:ok, clicked_char_settings} <-
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id),
|
||||
{:ok, settings} <- update_follow_status(map_id, clicked_char, clicked_char_settings) do
|
||||
{:ok, settings}
|
||||
end
|
||||
end
|
||||
|
||||
defp update_follow_status(map_id, clicked_char, nil) do
|
||||
# Create new settings with tracked=true and followed=true
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: clicked_char.id,
|
||||
map_id: map_id,
|
||||
tracked: true,
|
||||
followed: true
|
||||
})
|
||||
end
|
||||
|
||||
defp update_follow_status(map_id, clicked_char, clicked_char_settings) do
|
||||
followed = !clicked_char_settings.followed
|
||||
|
||||
with :ok <- maybe_unfollow_others(map_id, clicked_char.id, followed),
|
||||
:ok <- maybe_track_character(clicked_char_settings, followed),
|
||||
{:ok, settings} <- update_follow(clicked_char_settings, followed) do
|
||||
{:ok, settings}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_unfollow_others(_map_id, _char_id, false), do: :ok
|
||||
|
||||
defp maybe_unfollow_others(map_id, char_id, true) do
|
||||
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
all_settings
|
||||
|> Enum.filter(&(&1.character_id != char_id && &1.followed))
|
||||
|> Enum.each(&WandererApp.MapCharacterSettingsRepo.unfollow/1)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp maybe_track_character(settings, false), do: :ok
|
||||
|
||||
defp maybe_track_character(settings, true) do
|
||||
if not settings.tracked do
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.track(settings)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_follow(settings, true), do: WandererApp.MapCharacterSettingsRepo.follow(settings)
|
||||
defp update_follow(settings, false), do: WandererApp.MapCharacterSettingsRepo.unfollow(settings)
|
||||
|
||||
defp build_tracking_data(map_id, current_user) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
map <- Ash.load!(map, :acls),
|
||||
{:ok, character_settings} <- WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id) do
|
||||
tracking_data =
|
||||
Enum.map(characters_with_access, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
tracked = if setting, do: setting.tracked, else: false
|
||||
followed = if setting, do: setting.followed, else: false
|
||||
|
||||
%{
|
||||
id: "#{char.id}",
|
||||
name: char.name,
|
||||
eve_id: char.eve_id,
|
||||
portrait_url: EVEUtil.get_portrait_url(char.eve_id),
|
||||
corporation_ticker: char.corporation_ticker,
|
||||
alliance_ticker: Map.get(char, :alliance_ticker, ""),
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, tracking_data}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCharactersEventHandler, MapSystemsEventHandler}
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
|
||||
def handle_server_event(:update_permissions, socket) do
|
||||
DebounceAndThrottle.Debounce.apply(
|
||||
@@ -123,8 +124,12 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_server_event(%{payload: kill_data}, socket) when is_map(kill_data) and map_size(kill_data) > 0 do
|
||||
def handle_server_event(%{event: :detailed_kills_updated, payload: payload}, socket) do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"detailed_kills_updated",
|
||||
payload
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(event, socket) do
|
||||
@@ -249,6 +254,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
)
|
||||
|> MapCharactersEventHandler.add_character()}
|
||||
|
||||
def handle_ui_event(
|
||||
"show_tracking",
|
||||
_,
|
||||
socket
|
||||
),
|
||||
do: MapCharactersEventHandler.handle_ui_event("show_tracking", %{}, socket)
|
||||
|
||||
def handle_ui_event(event, body, socket) do
|
||||
Logger.debug(fn -> "unhandled map ui event: #{inspect(event)} #{inspect(body)}" end)
|
||||
{:noreply, socket}
|
||||
@@ -273,8 +285,23 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
user_permissions: user_permissions,
|
||||
name: map_name,
|
||||
owner_id: owner_id
|
||||
} = map
|
||||
} = _map
|
||||
) do
|
||||
with {:ok, init_data} <- setup_map_data(map_id, current_user, user_permissions, owner_id),
|
||||
:ok <- check_map_access(init_data, only_tracked_characters) do
|
||||
setup_map_socket(socket, map_id, map_slug, map_name, init_data, only_tracked_characters)
|
||||
else
|
||||
{:error, :not_all_tracked} ->
|
||||
Process.send_after(self(), :not_all_characters_tracked, 10)
|
||||
socket
|
||||
|
||||
_ ->
|
||||
Process.send_after(self(), :no_permissions, 10)
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp setup_map_data(map_id, current_user, user_permissions, owner_id) do
|
||||
user_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
user_permissions,
|
||||
@@ -282,60 +309,81 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
current_user.characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
{:ok, map_user_settings} = WandererApp.MapUserSettingsRepo.get(map_id, current_user.id)
|
||||
with {:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user.id),
|
||||
{:ok, character_settings} <- get_character_settings(map_id),
|
||||
{:ok, available_map_characters} <-
|
||||
WandererApp.Maps.get_tracked_map_characters(map_id, current_user) do
|
||||
tracked_data = get_tracked_data(available_map_characters, character_settings)
|
||||
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, %{characters: availaible_map_characters}} =
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id)
|
||||
|
||||
can_view? = user_permissions.view_system
|
||||
can_track? = user_permissions.track_character
|
||||
|
||||
tracked_character_ids =
|
||||
availaible_map_characters |> Enum.filter(& &1.tracked) |> Enum.map(& &1.id)
|
||||
|
||||
all_character_tracked? =
|
||||
not (availaible_map_characters |> Enum.empty?()) and
|
||||
availaible_map_characters |> Enum.all?(& &1.tracked)
|
||||
|
||||
cond do
|
||||
(only_tracked_characters and can_track? and all_character_tracked?) or
|
||||
(not only_tracked_characters and can_view?) ->
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
|
||||
{:ok, ui_loaded} = WandererApp.Cache.get_and_remove("map_#{map_slug}:ui_loaded", false)
|
||||
|
||||
if ui_loaded do
|
||||
maybe_start_map(map_id)
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings,
|
||||
page_title: map_name,
|
||||
user_permissions: user_permissions,
|
||||
tracked_character_ids: tracked_character_ids,
|
||||
only_tracked_characters: only_tracked_characters
|
||||
)
|
||||
|
||||
only_tracked_characters and can_track? and not all_character_tracked? ->
|
||||
Process.send_after(self(), :not_all_characters_tracked, 10)
|
||||
socket
|
||||
|
||||
true ->
|
||||
Process.send_after(self(), :no_permissions, 10)
|
||||
socket
|
||||
{:ok,
|
||||
%{
|
||||
user_permissions: user_permissions,
|
||||
map_user_settings: map_user_settings,
|
||||
tracked_character_ids: tracked_data.tracked_ids,
|
||||
all_character_tracked?: tracked_data.all_tracked?,
|
||||
can_view?: user_permissions.view_system,
|
||||
can_track?: user_permissions.track_character
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp init_map(socket, _map) do
|
||||
Process.send_after(self(), :no_access, 10)
|
||||
defp get_character_settings(map_id) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_tracked_data(available_map_characters, character_settings) do
|
||||
tracked_ids =
|
||||
available_map_characters
|
||||
|> Enum.filter(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting != nil && setting.tracked == true
|
||||
end)
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
all_tracked? =
|
||||
not Enum.empty?(available_map_characters) and
|
||||
Enum.all?(available_map_characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting != nil && setting.tracked == true
|
||||
end)
|
||||
|
||||
%{tracked_ids: tracked_ids, all_tracked?: all_tracked?}
|
||||
end
|
||||
|
||||
defp check_map_access(
|
||||
%{can_view?: true, can_track?: can_track?, all_character_tracked?: all_tracked?},
|
||||
only_tracked_characters
|
||||
) do
|
||||
cond do
|
||||
only_tracked_characters and can_track? and all_tracked? -> :ok
|
||||
not only_tracked_characters -> :ok
|
||||
only_tracked_characters and can_track? -> {:error, :not_all_tracked}
|
||||
true -> {:error, :no_permissions}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_map_access(_, _), do: {:error, :no_permissions}
|
||||
|
||||
defp setup_map_socket(socket, map_id, map_slug, map_name, init_data, only_tracked_characters) do
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
|
||||
{:ok, ui_loaded} = WandererApp.Cache.get_and_remove("map_#{map_slug}:ui_loaded", false)
|
||||
|
||||
if ui_loaded do
|
||||
maybe_start_map(map_id)
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
map_id: map_id,
|
||||
map_user_settings: init_data.map_user_settings,
|
||||
page_title: map_name,
|
||||
user_permissions: init_data.user_permissions,
|
||||
tracked_character_ids: init_data.tracked_character_ids,
|
||||
only_tracked_characters: only_tracked_characters
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_map_server_started(
|
||||
@@ -457,7 +505,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
) do
|
||||
socket =
|
||||
socket
|
||||
|> handle_map_start_events(map_id, events)
|
||||
|> MapCharactersEventHandler.handle_tracking_events(map_id, events)
|
||||
|
||||
{:ok, options} =
|
||||
map_id
|
||||
@@ -467,22 +515,20 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
initial_data
|
||||
|> Map.get(:user_permissions)
|
||||
|
||||
# Use the current user directly without reloading
|
||||
current_user = socket.assigns.current_user
|
||||
|
||||
map_characters =
|
||||
map_id
|
||||
|> WandererApp.Map.list_characters()
|
||||
|> filter_map_characters(user_character_eve_ids, user_permissions, options)
|
||||
|> Enum.map(&MapCharactersEventHandler.map_ui_character/1)
|
||||
|
||||
has_tracked_characters? =
|
||||
MapCharactersEventHandler.has_tracked_characters?(user_character_eve_ids)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
map_loaded?: true,
|
||||
is_subscription_active?: Map.get(initial_data, :is_subscription_active, false),
|
||||
user_characters: user_character_eve_ids,
|
||||
has_tracked_characters?: has_tracked_characters?
|
||||
is_subscription_active?: Map.get(initial_data, :is_subscription_active, false)
|
||||
)
|
||||
|> MapEventHandler.push_map_event(
|
||||
"init",
|
||||
@@ -494,50 +540,26 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
attr: "data-loaded"
|
||||
})
|
||||
|
||||
case not has_tracked_characters? && user_permissions.track_character do
|
||||
true ->
|
||||
socket
|
||||
|> MapCharactersEventHandler.add_character()
|
||||
# Initialize character tracking
|
||||
{socket, needs_tracking_setup} =
|
||||
MapCharactersEventHandler.init_character_tracking(
|
||||
socket,
|
||||
map_id,
|
||||
%{
|
||||
current_user: current_user,
|
||||
user_permissions: user_permissions
|
||||
}
|
||||
)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
if not socket.assigns.has_tracked_characters? && user_permissions.track_character &&
|
||||
not needs_tracking_setup do
|
||||
socket
|
||||
|> MapCharactersEventHandler.add_character()
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_map_start_events(socket, map_id, events) do
|
||||
events
|
||||
|> Enum.reduce(socket, fn event, socket ->
|
||||
case event do
|
||||
{:track_characters, map_characters, track_character} ->
|
||||
:ok =
|
||||
MapCharactersEventHandler.track_characters(map_characters, map_id, track_character)
|
||||
|
||||
:ok = MapCharactersEventHandler.add_characters(map_characters, map_id, track_character)
|
||||
socket
|
||||
|
||||
:invalid_token_message ->
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"One of your characters has expired token. Please refresh it on characters page."
|
||||
)
|
||||
|
||||
:empty_tracked_characters ->
|
||||
socket
|
||||
|
||||
:map_character_limit ->
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Map reached its character limit, your characters won't be tracked. Please contact administrator."
|
||||
)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_map_data(map_id, include_static_data? \\ true) do
|
||||
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
|
||||
{:ok, connections} = map_id |> WandererApp.Map.list_connections()
|
||||
@@ -575,4 +597,22 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
user_character_eve_ids |> Enum.member?(character.eve_id)
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_task_result(socket, {:activity_data, activity_data}),
|
||||
do: MapCharactersEventHandler.handle_activity_data(socket, activity_data)
|
||||
|
||||
defp handle_task_result(socket, {:ok, %{type: type} = result})
|
||||
when type in [
|
||||
:character_activity,
|
||||
:character_tracking,
|
||||
:character_settings,
|
||||
:character_location,
|
||||
:character_online,
|
||||
:character_ship,
|
||||
:character_fleet
|
||||
] do
|
||||
MapCharactersEventHandler.handle_character_result(socket, type, result)
|
||||
end
|
||||
|
||||
defp handle_task_result(socket, _), do: socket
|
||||
end
|
||||
|
||||
@@ -20,14 +20,16 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
:character_removed,
|
||||
:character_updated,
|
||||
:characters_updated,
|
||||
:present_characters_updated
|
||||
:present_characters_updated,
|
||||
:tracking_characters_data
|
||||
]
|
||||
|
||||
@map_characters_ui_events [
|
||||
"add_character",
|
||||
"toggle_track",
|
||||
"toggle_follow",
|
||||
"hide_tracking"
|
||||
"hide_tracking",
|
||||
"show_tracking"
|
||||
]
|
||||
|
||||
@map_system_events [
|
||||
@@ -76,7 +78,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
]
|
||||
|
||||
@map_activity_events [
|
||||
:character_activity
|
||||
:character_activity,
|
||||
:character_activity_data
|
||||
]
|
||||
|
||||
@map_activity_ui_events [
|
||||
@@ -141,7 +144,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
|
||||
def handle_event(socket, %{event: event_name} = event)
|
||||
when event_name in @map_activity_events,
|
||||
do: MapActivityEventHandler.handle_server_event(event, socket)
|
||||
do: MapCharactersEventHandler.handle_server_event(event, socket)
|
||||
|
||||
def handle_event(socket, %{event: event_name} = event)
|
||||
when event_name in @map_routes_events,
|
||||
@@ -185,6 +188,12 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
Process.send_after(self(), map_error, 100)
|
||||
socket
|
||||
|
||||
{:activity_data, activity_data} ->
|
||||
MapCharactersEventHandler.handle_server_event(
|
||||
%{event: :character_activity_data, payload: {:activity_data, activity_data}},
|
||||
socket
|
||||
)
|
||||
|
||||
{event, payload} ->
|
||||
Process.send_after(
|
||||
self(),
|
||||
@@ -195,10 +204,21 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
socket
|
||||
|
||||
_ ->
|
||||
Logger.warning("Unhandled task result: #{inspect(result)}")
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(socket, {:DOWN, ref, :process, _pid, reason}) when is_reference(ref) do
|
||||
# Task failed, log the error and update the client
|
||||
Logger.error("Task failed: #{inspect(reason)}")
|
||||
|
||||
MapCharactersEventHandler.handle_server_event(
|
||||
%{event: :character_activity_data, payload: []},
|
||||
socket
|
||||
)
|
||||
end
|
||||
|
||||
def handle_event(socket, event),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
|
||||
@@ -228,7 +248,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
|
||||
def handle_ui_event(event, body, socket)
|
||||
when event in @map_activity_ui_events,
|
||||
do: MapActivityEventHandler.handle_ui_event(event, body, socket)
|
||||
do: MapCharactersEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
def handle_ui_event(
|
||||
event,
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
event_name="change_map"
|
||||
/>
|
||||
|
||||
<.button
|
||||
:if={(@user_permissions || %{}) |> Map.get(:view_character, false)}
|
||||
<button
|
||||
:if={(@user_permissions || %{}) |> Map.get(:track_character, false)}
|
||||
id="show-activity-button"
|
||||
class="h-8 w-8 hover:text-white"
|
||||
phx-click="show_activity"
|
||||
class="btn-link -mt-1 text-gray-400 hover:text-white"
|
||||
>
|
||||
<.icon name="hero-chart-bar-solid" class="w-6 h-6" />
|
||||
</.button>
|
||||
</button>
|
||||
|
||||
<.link
|
||||
:if={(@user_permissions || %{}) |> Map.get(:delete_map, false)}
|
||||
@@ -39,89 +40,3 @@
|
||||
<.icon name="hero-user-group-solid" class="w-6 h-6" />
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<.modal
|
||||
:if={assigns |> Map.get(:show_activity?, false)}
|
||||
id="map-activity-modal"
|
||||
title="Activity of Characters"
|
||||
class="!w-[500px]"
|
||||
show
|
||||
on_cancel={JS.push("hide_activity")}
|
||||
>
|
||||
<.async_result :let={character_activity} assign={@character_activity}>
|
||||
<:loading>Loading...</:loading>
|
||||
<:failed :let={reason}><%= reason %></:failed>
|
||||
|
||||
<span :if={character_activity}>
|
||||
<.live_component
|
||||
module={WandererAppWeb.CharacterActivity}
|
||||
id="character-activity"
|
||||
activity={character_activity}
|
||||
notify_to={self()}
|
||||
/>
|
||||
</span>
|
||||
</.async_result>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={assigns |> Map.get(:show_tracking?, false)}
|
||||
id="map-tracking-modal"
|
||||
title="Track and Follow Characters"
|
||||
show
|
||||
on_cancel={JS.push("hide_tracking")}
|
||||
>
|
||||
<.async_result :let={characters} assign={@characters}>
|
||||
<:loading>
|
||||
<span class="loading loading-dots loading-xs" />
|
||||
</:loading>
|
||||
<:failed :let={reason}>
|
||||
<%= reason %>
|
||||
</:failed>
|
||||
|
||||
<.table
|
||||
:if={characters}
|
||||
id="characters-tracking-table"
|
||||
class="h-[400px] !overflow-y-auto"
|
||||
rows={characters}
|
||||
>
|
||||
<:col :let={character} label="Track">
|
||||
<label class="flex items-center gap-2 justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
phx-click="toggle_track"
|
||||
phx-value-character-id={character.id}
|
||||
id={"character-track-#{character.id}"}
|
||||
checked={character.tracked}
|
||||
/>
|
||||
</label>
|
||||
</:col>
|
||||
<:col :let={character} label="Follow">
|
||||
<label class="flex items-center gap-2 justify-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="followed_character"
|
||||
class="radio"
|
||||
phx-click="toggle_follow"
|
||||
phx-value-character-id={character.id}
|
||||
checked={Map.get(character, :followed, false)}
|
||||
/>
|
||||
</label>
|
||||
</:col>
|
||||
<:col :let={character} label="Character">
|
||||
<div class="flex items-center gap-3">
|
||||
<.avatar url={member_icon_url(character.eve_id)} label={character.name} />
|
||||
<div>
|
||||
<div class="font-bold">
|
||||
<%= character.name %>
|
||||
<span class="ml-1 text-gray-400">
|
||||
[<%= character.corporation_ticker %>]
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm opacity-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</:col>
|
||||
</.table>
|
||||
</.async_result>
|
||||
</.modal>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
%{
|
||||
title: "User Guide: Public API Endpoints for Map Data",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/01-05-map-public-api/generate-key.png",
|
||||
tags: ~w(map public-api guide interface),
|
||||
description: "Learn how to use the Wanderer public API endpoints to retrieve system and character data from your map. This guide covers available endpoints, request examples, and sample responses."
|
||||
title: "User Guide: Public API Endpoints for Map Data",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/01-05-map-public-api/generate-key.png",
|
||||
tags: ~w(map public-api guide interface),
|
||||
description: "Learn how to use the Wanderer public API endpoints to retrieve system and character data from your map. This guide covers available endpoints, request examples, and sample responses."
|
||||
}
|
||||
|
||||
---
|
||||
@@ -24,7 +24,7 @@ Each request to the Wanderer APIs that being with /api/map must include a valid
|
||||
|
||||
Authorization: Bearer <YOUR_MAP_API_KEY>
|
||||
|
||||
If the API key is missing or incorrect, you’ll receive a `401 Unauthorized` response.
|
||||
If the API key is missing or incorrect, you'll receive a `401 Unauthorized` response.
|
||||
|
||||
No api key is required for routes that being with /api/common
|
||||
|
||||
@@ -42,13 +42,16 @@ No api key is required for routes that being with /api/common
|
||||
- **Parameters:**
|
||||
- `map_id` (optional if `slug` is provided) — the UUID of the map.
|
||||
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
|
||||
- `all=true` (optional) — if set, returns *all* systems instead of only "visible" systems.
|
||||
- `all=true` (optional) — if set, returns _all_ systems instead of only "visible" systems.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/systems?slug=some-slug"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": [
|
||||
@@ -73,6 +76,7 @@ No api key is required for routes that being with /api/common
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Show Single System
|
||||
@@ -86,10 +90,13 @@ No api key is required for routes that being with /api/common
|
||||
- **Authentication:** Required via `Authorization` header.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/system?id=<REDACTED_NUMBER>&slug=<REDACTED_SLUG>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": {
|
||||
@@ -111,6 +118,7 @@ No api key is required for routes that being with /api/common
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Show Single System Static Info
|
||||
@@ -122,10 +130,13 @@ No api key is required for routes that being with /api/common
|
||||
- **Authentication:** No API token required
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl "https://wanderer.example.com/api/common/static-system-info?id=31002229
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": {
|
||||
@@ -157,6 +168,7 @@ No api key is required for routes that being with /api/common
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. List Tracked Characters
|
||||
@@ -168,10 +180,13 @@ No api key is required for routes that being with /api/common
|
||||
- **Authentication:** Required via `Authorization` header.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/characters?slug=some-slug"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": [
|
||||
@@ -197,6 +212,7 @@ No api key is required for routes that being with /api/common
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Kills Activity
|
||||
@@ -207,10 +223,13 @@ No api key is required for routes that being with /api/common
|
||||
- **Description:** Retrieves the kill activity for the specified map (by `map_id` or `slug`), including details on the attacker and victim
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/systems-kills?slug==some-slug"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": [
|
||||
@@ -292,11 +311,52 @@ No api key is required for routes that being with /api/common
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Character Activity
|
||||
|
||||
GET /api/map/activity?map_id=<UUID>
|
||||
GET /api/map/activity?slug=<map-slug>
|
||||
|
||||
- **Description:** Retrieves activity statistics for all characters on the specified map, including connections made, passages through systems, and signatures added.
|
||||
- **Authentication:** Required via `Authorization` header.
|
||||
- **Parameters:**
|
||||
- `map_id` (optional if `slug` is provided) — the UUID of the map.
|
||||
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/activity?slug=some-slug"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"character": {
|
||||
"name": "Character Name",
|
||||
"corporation_ticker": "CORP",
|
||||
"alliance_ticker": "ALLY",
|
||||
"eve_id": "12345"
|
||||
},
|
||||
"connections": 4,
|
||||
"passages": 28,
|
||||
"signatures": 1
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Using these APIs, you can programmatically retrieve system and character information from your map. Whether you’re building a custom analytics dashboard, a corp management tool, or just want to explore data outside the standard UI, these endpoints provide a straightforward way to fetch up-to-date map details.
|
||||
Using these APIs, you can programmatically retrieve system and character information from your map. Whether you're building a custom analytics dashboard, a corp management tool, or just want to explore data outside the standard UI, these endpoints provide a straightforward way to fetch up-to-date map details.
|
||||
|
||||
For questions or additional support, please reach out to the Wanderer Team.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user