Compare commits

..

1 Commits

Author SHA1 Message Date
Dmitry Popov
2809959056 feat(Core): Show tracking for new userfeat(Map): Add minimap options 2024-12-20 16:07:27 +01:00
33 changed files with 254 additions and 732 deletions

View File

@@ -2,42 +2,6 @@
<!-- changelog -->
## [v1.32.2](https://github.com/wanderer-industries/wanderer/compare/v1.32.1...v1.32.2) (2025-01-02)
## [v1.32.1](https://github.com/wanderer-industries/wanderer/compare/v1.32.0...v1.32.1) (2024-12-25)
## [v1.32.0](https://github.com/wanderer-industries/wanderer/compare/v1.31.0...v1.32.0) (2024-12-24)
### Features:
* Map: Add search & update manual adding systems API
* Map: Add search & update manual adding systems API
### Bug Fixes:
* Map: Added ability to add new system to routes via routes widget
* Map: Reworked add system to map
## [v1.31.0](https://github.com/wanderer-industries/wanderer/compare/v1.30.2...v1.31.0) (2024-12-20)
### Features:
* Core: Show tracking for new users by default. Auto link characters to account fix. Add character loading indicators.
## [v1.30.2](https://github.com/wanderer-industries/wanderer/compare/v1.30.1...v1.30.2) (2024-12-17)

View File

@@ -108,7 +108,3 @@
.p-dropdown-empty-message {
padding: 0.25rem 0.5rem;
}
.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token {
margin-right: 0 !important;
}

View File

@@ -29,7 +29,7 @@ import {
useContextMenuConnectionHandlers,
useContextMenuRootHandlers,
} from './components';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
@@ -92,7 +92,6 @@ interface MapCompProps {
onSelectionChange: OnMapSelectionChange;
onManualDelete(systems: string[]): void;
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
minimapClasses?: string;
isShowMinimap?: boolean;
@@ -117,7 +116,6 @@ const MapComp = ({
isThickConnections,
isShowBackgroundPattern,
isSoftBackground,
onAddSystem,
}: MapCompProps) => {
const { getNode } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -125,7 +123,7 @@ const MapComp = ({
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers();
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();

View File

@@ -1,16 +1,14 @@
import { useReactFlow, XYPosition } from 'reactflow';
import React, { useCallback, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { useMapState } from '../../MapProvider.tsx';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
type UseContextMenuRootHandlers = {
onAddSystem?: OnMapAddSystemCallback;
};
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
export const useContextMenuRootHandlers = () => {
const rf = useReactFlow();
const contextMenuRef = useRef<ContextMenu | null>(null);
const { outCommand } = useMapState();
const [position, setPosition] = useState<XYPosition | null>(null);
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -20,17 +18,14 @@ export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHa
contextMenuRef.current?.show(e);
};
const ref = useRef({ onAddSystem, position });
ref.current = { onAddSystem, position };
const onAddSystemCallback = useCallback(() => {
ref.current.onAddSystem?.({ coordinates: position });
}, [position]);
const onAddSystem = () => {
outCommand({ type: OutCommand.manualAddSystem, data: { coordinates: position } });
};
return {
handleRootContext,
contextMenuRef,
onAddSystem: onAddSystemCallback,
onAddSystem,
};
};

View File

@@ -13,11 +13,11 @@
stroke-width: 2px;
&.MassVerge:not(&.Frigate) {
stroke: #af0000;
stroke: #af2900;
}
&.MassHalf:not(&.Frigate) {
stroke: #ffd700;
stroke: #a85f00;
}
&.Frigate {

View File

@@ -1,6 +1,5 @@
import { SolarSystemRawType } from '@/hooks/Mapper/types/system';
import { SolarSystemConnection } from '@/hooks/Mapper/types';
import { XYPosition } from 'reactflow';
export type MapSolarSystemType = Omit<SolarSystemRawType, 'position'>;
@@ -8,5 +7,3 @@ export type OnMapSelectionChange = (event: {
systems: string[];
connections: Pick<SolarSystemConnection, 'source' | 'target'>[];
}) => void;
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;

View File

@@ -1,10 +0,0 @@
.SearchItem {
& > * {
font-size: 13px !important;
}
}
.SearchItemEffect {
font-weight: initial !important;
}

View File

@@ -1,203 +0,0 @@
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import classes from './AddSystemDialog.module.scss';
import clsx from 'clsx';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
interface AddSystemDialogProps {
title?: string;
visible: boolean;
setVisible: (visible: boolean) => void;
onSubmit?: SearchOnSubmitCallback;
excludedSystems?: number[];
}
export const AddSystemDialog = ({
title = 'Add system',
visible,
setVisible,
onSubmit,
excludedSystems = [],
}: AddSystemDialogProps) => {
const {
outCommand,
data: { wormholesData },
} = useMapRootState();
const inputRef = useRef<any>();
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
const [filteredItems, setFilteredItems] = useState<SearchSystemItem[]>([]);
const [selectedItem, setSelectedItem] = useState<SearchSystemItem[] | null>(null);
const searchItems = useCallback(
async (event: { query: string }) => {
if (event.query.length < 2) {
setFilteredItems([]);
return;
}
const query = event.query;
if (query.length === 0) {
setFilteredItems([]);
} else {
try {
const result = await outCommand({
type: OutCommand.searchSystems,
data: {
text: query,
},
});
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
const amatch = a.label.indexOf(query);
const bmatch = b.label.indexOf(query);
return amatch - bmatch;
});
if (excludedSystems) {
prepared = prepared.filter(x => !excludedSystems.includes(x.system_static_info.solar_system_id));
}
setFilteredItems(prepared);
} catch (error) {
console.error('Error fetching data:', error);
setFilteredItems([]);
}
}
},
[excludedSystems, outCommand],
);
const ref = useRef({ onSubmit, selectedItem });
ref.current = { onSubmit, selectedItem };
const handleSubmit = useCallback(() => {
const { onSubmit, selectedItem } = ref.current;
setFilteredItems([]);
setSelectedItem([]);
if (!selectedItem) {
setVisible(false);
return;
}
onSubmit?.(selectedItem[0]);
setVisible(false);
}, [setVisible]);
return (
<Dialog
header={title}
visible={visible}
draggable={false}
style={{ width: '520px' }}
onShow={onShow}
onHide={() => {
if (!visible) {
return;
}
setVisible(false);
}}
>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</Dialog>
);
};

View File

@@ -1 +0,0 @@
export * from './AddSystemDialog';

View File

@@ -21,11 +21,6 @@ import { RoutesProvider, useRouteProvider } from './RoutesProvider.tsx';
import { ContextMenuSystemInfo, useContextMenuSystemInfoHandlers } from '@/hooks/Mapper/components/contexts';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { OutCommand } from '@/hooks/Mapper/types';
const sortByDist = (a: Route, b: Route) => {
const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
@@ -168,12 +163,6 @@ export const RoutesWidgetContent = () => {
export const RoutesWidgetComp = () => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update } = useRouteProvider();
const {
data: { hubs = [] },
outCommand,
} = useMapRootState();
const preparedHubs = useMemo(() => hubs.map(x => parseInt(x)), [hubs]);
const isSecure = data.path_type === 'secure';
const handleSecureChange = useCallback(() => {
@@ -185,23 +174,6 @@ export const RoutesWidgetComp = () => {
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 155);
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (preparedHubs.includes(item.value)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: item.value },
});
},
[hubs, outCommand],
);
return (
<Widget
@@ -209,14 +181,6 @@ export const RoutesWidgetComp = () => {
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">Routes</span>
<LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
onClick={onAddSystem}
tooltip={{
content: 'Click here to add new system to routes',
}}
/>
<WdTooltipWrapper content="Show shortest route">
<WdCheckbox
size="xs"
@@ -227,26 +191,13 @@ export const RoutesWidgetComp = () => {
classNameLabel={clsx('text-red-400')}
/>
</WdTooltipWrapper>
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={() => setRouteSettingsVisible(true)}
tooltip={{
content: 'Click here to open Routes settings',
}}
/>
<WdImgButton className={PrimeIcons.SLIDERS_H} onClick={() => setRouteSettingsVisible(true)} />
</LayoutEventBlocker>
</div>
}
>
<RoutesWidgetContent />
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
</Widget>
);
};

View File

@@ -44,6 +44,7 @@ type CheckboxesList = {
const COMMON_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMinimap, label: 'Show Minimap' },
{ prop: InterfaceStoredSettingsProps.isStickMinimapToLeft, label: 'Stick Minimap to left' },
];
const SYSTEMS_CHECKBOXES_PROPS: CheckboxesList = [

View File

@@ -1,3 +1,8 @@
.MiniMap {
right: 3.5rem !important;
&--left {
left: 3.5rem !important;
width: 200px !important;
}
}

View File

@@ -1,8 +1,8 @@
import { Map } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import {
@@ -14,18 +14,14 @@ import classes from './MapWrapper.module.scss';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { Node, XYPosition } from 'reactflow';
import { Node } from 'reactflow';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
// TODO: INFO - this component needs for abstract work with Map instance
export const MapWrapper = () => {
@@ -36,6 +32,7 @@ export const MapWrapper = () => {
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
isStickMinimapToLeft = STORED_INTERFACE_DEFAULT_VALUES.isStickMinimapToLeft,
isShowKSpace,
isThickConnections,
isShowBackgroundPattern,
@@ -51,7 +48,6 @@ export const MapWrapper = () => {
const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
@@ -67,6 +63,18 @@ export const MapWrapper = () => {
runCommand(event);
});
const minimapClasses = useMemo(() => {
if (isStickMinimapToLeft) {
return classes['MiniMap--left'];
}
if (!isShowMenu) {
return classes.MiniMap;
}
return undefined;
}, [isShowMenu, isStickMinimapToLeft]);
const onSelectionChange: OnMapSelectionChange = useCallback(
({ systems, connections }) => {
const { selectedConnections, selectedSystems } = ref.current;
@@ -128,28 +136,6 @@ export const MapWrapper = () => {
}
}, []);
const onAddSystem: OnMapAddSystemCallback = useCallback(({ coordinates }) => {
setOpenAddSystem(coordinates);
}, []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
emitMapEvent({
name: Commands.centerSystem,
data: item.value.toString(),
});
return;
}
await outCommand({
type: OutCommand.manualAddSystem,
data: { coordinates: openAddSystem, solar_system_id: item.value },
});
},
[openAddSystem, outCommand],
);
return (
<>
<Map
@@ -159,14 +145,13 @@ export const MapWrapper = () => {
onConnectionInfoClick={handleConnectionDbClick}
onSystemContextMenu={handleSystemContextMenu}
onSelectionContextMenu={handleSystemMultipleContext}
minimapClasses={!isShowMenu ? classes.MiniMap : undefined}
minimapClasses={minimapClasses}
isShowMinimap={isShowMinimap}
showKSpaceBG={isShowKSpace}
onManualDelete={handleManualDelete}
isThickConnections={isThickConnections}
isShowBackgroundPattern={isShowBackgroundPattern}
isSoftBackground={isSoftBackground}
onAddSystem={onAddSystem}
/>
{openSettings != null && (
@@ -181,12 +166,6 @@ export const MapWrapper = () => {
<SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
)}
<AddSystemDialog
visible={!!openAddSystem}
setVisible={() => setOpenAddSystem(null)}
onSubmit={handleSubmitAddSystem}
/>
<Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} />
<ContextMenuSystem

View File

@@ -31,15 +31,12 @@ const prepareEffects = (effects: Record<string, EffectRaw>, effectName: string,
return out;
};
let counter = 0;
export interface WHEffectViewProps {
effectName: string;
effectPower: number;
className?: string;
}
export const WHEffectView = ({ effectName, effectPower, className }: WHEffectViewProps) => {
export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) => {
const {
data: { effects },
} = useMapRootState();
@@ -52,7 +49,7 @@ export const WHEffectView = ({ effectName, effectPower, className }: WHEffectVie
[effectName, effectPower, effects],
);
const targetClass = useMemo(() => `wh-effect-name${effectInfo.id}-${counter++}`, []);
const targetClass = `wh-effect-name${effectInfo.id}`;
return (
<div className={classes.WHEffectViewRoot}>
@@ -87,7 +84,7 @@ export const WHEffectView = ({ effectName, effectPower, className }: WHEffectVie
</div>
</FixedTooltip>
<div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass, className)}>
<div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass)}>
{effectName}
</div>
</div>

View File

@@ -32,6 +32,7 @@ const INITIAL_DATA: MapRootData = {
export enum InterfaceStoredSettingsProps {
isShowMenu = 'isShowMenu',
isShowMinimap = 'isShowMinimap',
isStickMinimapToLeft = 'isStickMinimapToLeft',
isShowKSpace = 'isShowKSpace',
isThickConnections = 'isThickConnections',
isShowUnsplashedSignatures = 'isShowUnsplashedSignatures',
@@ -42,6 +43,7 @@ export enum InterfaceStoredSettingsProps {
export type InterfaceStoredSettings = {
isShowMenu: boolean;
isShowMinimap: boolean;
isStickMinimapToLeft: boolean;
isShowKSpace: boolean;
isThickConnections: boolean;
isShowUnsplashedSignatures: boolean;
@@ -52,6 +54,7 @@ export type InterfaceStoredSettings = {
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false,
isShowMinimap: true,
isStickMinimapToLeft: false,
isShowKSpace: false,
isThickConnections: false,
isShowUnsplashedSignatures: false,

View File

@@ -153,7 +153,6 @@ export enum OutCommand {
getUserSettings = 'get_user_settings',
updateUserSettings = 'update_user_settings',
unlinkSignature = 'unlink_signature',
searchSystems = 'search_systems',
}
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;

View File

@@ -120,13 +120,3 @@ export type SolarSystemRawType = {
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];
};
export type SearchSystemItem = {
class_title: string;
constellation_name: string;
label: string;
region_name: string;
system_static_info: SolarSystemStaticInfoRaw;
value: number;
};

View File

@@ -14,24 +14,31 @@ defmodule WandererApp.Api.MapCharacterSettings do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:read_by_map, action: :read_by_map)
define(:by_map_filtered, action: :by_map_filtered)
define(:tracked_by_map_filtered, action: :tracked_by_map_filtered)
define(:tracked_by_map_all, action: :tracked_by_map_all)
define(:read_by_map,
action: :read_by_map
)
define(:by_map_filtered,
action: :by_map_filtered
)
define(:tracked_by_map_filtered,
action: :tracked_by_map_filtered
)
define(:tracked_by_map_all,
action: :tracked_by_map_all
)
define(:track, action: :track)
define(:untrack, action: :untrack)
define(:follow, action: :follow)
define(:unfollow, action: :unfollow)
end
actions do
default_accept [
:map_id,
:character_id,
:tracked,
:followed
:tracked
]
defaults [:create, :read, :update, :destroy]
@@ -69,14 +76,6 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :untrack do
change(set_attribute(:tracked, false))
end
update :follow do
change(set_attribute(:followed, true))
end
update :unfollow do
change(set_attribute(:followed, false))
end
end
attributes do
@@ -87,11 +86,6 @@ defmodule WandererApp.Api.MapCharacterSettings do
allow_nil? true
end
attribute :followed, :boolean do
default false
allow_nil? true
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -489,7 +489,7 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_location(
%{
character_id: character_id,
character_id: character_id
} =
state,
location

View File

@@ -15,12 +15,13 @@ defmodule WandererApp.Map.Server.CharactersImpl do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character,
followed: false
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
@@ -381,6 +382,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end

View File

@@ -333,7 +333,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id,
solar_system_id: location.solar_system_id
})
Impl.broadcast!(map_id, :add_connection, connection)
@@ -346,20 +346,9 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
:ok
{:error, :already_exists} ->
# Still broadcast location change in case of followed character
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
end
end
@@ -370,30 +359,33 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
def can_add_location(:none, _solar_system_id), do: false
def can_add_location(scope, solar_system_id) do
{:ok, system_static_info} = get_system_static_info(solar_system_id)
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
case scope do
:wormholes ->
not is_prohibited_system_class?(system_static_info.system_class) and
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
@wh_space |> Enum.member?(system_static_info.system_class)
:stargates ->
not is_prohibited_system_class?(system_static_info.system_class) and
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
@known_space |> Enum.member?(system_static_info.system_class)
:all ->
not is_prohibited_system_class?(system_static_info.system_class)
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class))
_ ->
false
end
end
def is_prohibited_system_class?(system_class) do
@prohibited_system_classes |> Enum.member?(system_class)
end
def is_connection_exist(map_id, from_solar_system_id, to_solar_system_id),
do:
not is_nil(
@@ -422,21 +414,24 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
current_system_id: to_solar_system_id
})
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(to_solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?() and to_solar_system_id != @jita and
from_solar_system_id != @jita
:stargates ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (known_jumps |> Enum.empty?())
end
end
@@ -452,16 +447,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
{:ok, system_static_info}
_ ->
{:ok, %{system_class: nil}}
end
end
defp maybe_remove_connection(map_id, location, old_location)
when not is_nil(location) and not is_nil(old_location) and
location.solar_system_id != old_location.solar_system_id do

View File

@@ -84,22 +84,20 @@ defmodule WandererApp.Maps do
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: false,
followed: false
tracked: false
}
def map_character(
%{name: name, id: id, eve_id: eve_id, corporation_ticker: corporation_ticker} =
_character,
%{tracked: tracked, followed: followed} = _character_settings
%{tracked: tracked} = _character_settings
),
do: %{
name: name,
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: tracked,
followed: followed
tracked: tracked
}
@decorate cacheable(

View File

@@ -24,31 +24,11 @@ defmodule WandererApp.MapCharacterSettingsRepo do
def get_tracked_by_map_all(map_id),
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
def get_by_map(map_id, character_id) do
case get_by_map_filtered(map_id, [character_id]) do
{:ok, [setting | _]} ->
{:ok, setting}
{:ok, []} ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
def track(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track()
def untrack(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack()
def track!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track!()
def untrack!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack!()
def follow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow()
def unfollow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow()
def follow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow!()
def unfollow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow!()
def destroy!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.destroy!()
end

View File

@@ -86,8 +86,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: selected_map.id,
tracked: true,
followed: false
tracked: true
})
{:noreply, socket}

View File

@@ -111,8 +111,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: true,
followed: false
tracked: true
})
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
@@ -191,92 +190,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)}
end
def handle_ui_event(
"toggle_follow",
%{"character-id" => clicked_char_id},
%{
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} = 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(my_settings, &(&1.character_id == clicked_char_id))
{:ok, target_setting} =
if 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)
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)
end
# re-fetch or re-map to confirm final results in UI
%{result: characters} = socket.assigns.characters
{:ok, tracked_characters} = 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)
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}}
end)
|> MapEventHandler.push_map_event("init", %{user_characters: user_eve_ids, reset: false})
{:noreply, socket}
end
def handle_ui_event("hide_tracking", _, socket),
do: {:noreply, socket |> assign(show_tracking?: false)}

View File

@@ -3,7 +3,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
use Phoenix.Component
require Logger
alias WandererAppWeb.{MapEventHandler, MapCharactersEventHandler, MapSystemsEventHandler}
alias WandererAppWeb.{MapEventHandler, MapCharactersEventHandler}
def handle_server_event(:update_permissions, socket) do
DebounceAndThrottle.Debounce.apply(
@@ -147,7 +147,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
options =
WandererApp.Api.MapSolarSystem.find_by_name!(%{name: text})
|> Enum.take(100)
|> Enum.map(&MapSystemsEventHandler.map_system/1)
|> Enum.map(&map_system/1)
send_update(LiveSelect.Component, options: options, id: id)
@@ -162,14 +162,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
)
def handle_ui_event("toggle_follow_" <> character_id, _, socket),
do:
MapCharactersEventHandler.handle_ui_event(
"toggle_follow",
%{"character-id" => character_id},
socket
)
def handle_ui_event(
"get_user_settings",
_,
@@ -572,4 +564,21 @@ defmodule WandererAppWeb.MapCoreEventHandler do
user_character_eve_ids |> Enum.member?(character.eve_id)
end)
end
defp map_system(
%{
solar_system_name: solar_system_name,
constellation_name: constellation_name,
region_name: region_name,
solar_system_id: solar_system_id,
class_title: class_title
} = _system
),
do: %{
label: solar_system_name,
value: solar_system_id,
constellation_name: constellation_name,
region_name: region_name,
class_title: class_title
}
end

View File

@@ -21,57 +21,32 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|> MapEventHandler.push_map_event("remove_systems", solar_system_ids)
def handle_server_event(
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_id: map_id, map_user_settings: map_user_settings}} = socket
) do
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_user_settings: map_user_settings}} = socket
) do
is_user_character =
current_user.characters |> Enum.map(& &1.id) |> Enum.member?(character_id)
is_user_character =
current_user.characters
|> Enum.map(& &1.id)
|> Enum.member?(character_id)
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_followed =
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
{:ok, setting} -> setting.followed == true
_ -> false
end
must_select? = is_user_character && (is_select_on_spash || is_followed)
if not must_select? do
(is_user_character && is_select_on_spash)
|> case do
true ->
socket
else
# Check if we already selected this exact system for this char:
last_selected =
WandererApp.Cache.lookup!(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
nil
)
|> MapEventHandler.push_map_event("select_system", solar_system_id)
if last_selected == solar_system_id do
# same system => skip
socket
else
# new system => update cache + push event
WandererApp.Cache.put(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
solar_system_id
)
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
end
end
false ->
socket
end
end
def handle_server_event(%{event: :kills_updated, payload: kills}, socket) do
@@ -90,32 +65,55 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
do: MapCoreEventHandler.handle_server_event(event, socket)
def handle_ui_event(
"manual_add_system",
%{"solar_system_id" => solar_system_id, "coordinates" => coordinates} = _event,
"add_system",
%{"system_id" => [solar_system_id]} = _event,
%{
assigns: %{
current_user: current_user,
has_tracked_characters?: true,
map_id: map_id,
tracked_character_ids: tracked_character_ids,
user_permissions: %{add_system: true}
}
} =
socket
) do
assigns:
%{
map_id: map_id,
map_slug: map_slug,
current_user: current_user,
tracked_character_ids: tracked_character_ids,
user_permissions: %{add_system: true}
} = assigns
} = socket
)
when is_binary(solar_system_id) and solar_system_id != "" do
coordinates = Map.get(assigns, :coordinates)
WandererApp.Map.Server.add_system(
map_id,
%{
solar_system_id: solar_system_id,
solar_system_id: solar_system_id |> String.to_integer(),
coordinates: coordinates
},
current_user.id,
tracked_character_ids |> List.first()
)
{:noreply, socket}
{:noreply,
socket
|> push_patch(to: ~p"/#{map_slug}")}
end
def handle_ui_event(
"manual_add_system",
%{"coordinates" => coordinates} = _event,
%{
assigns: %{
has_tracked_characters?: true,
map_slug: map_slug,
user_permissions: %{add_system: true}
}
} =
socket
),
do:
{:noreply,
socket
|> assign(coordinates: coordinates)
|> push_patch(to: ~p"/#{map_slug}/add-system")}
def handle_ui_event(
"add_hub",
%{"system_id" => solar_system_id} = _event,
@@ -282,25 +280,6 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
{:reply, %{system_static_infos: system_static_infos}, socket}
end
def handle_ui_event(
"search_systems",
%{"text" => text} = _event,
socket
) do
systems =
WandererApp.Api.MapSolarSystem.find_by_name!(%{name: text})
|> Enum.take(100)
|> Enum.map(&map_system/1)
|> Enum.filter(fn system ->
not is_nil(system) && not is_nil(system.system_static_info) &&
not WandererApp.Map.Server.ConnectionsImpl.is_prohibited_system_class?(
system.system_static_info.system_class
)
end)
{:reply, %{systems: systems}, socket}
end
def handle_ui_event(
"delete_systems",
solar_system_ids,
@@ -328,27 +307,6 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
def map_system(
%{
solar_system_name: solar_system_name,
constellation_name: constellation_name,
region_name: region_name,
solar_system_id: solar_system_id,
class_title: class_title
} = _system
) do
system_static_info = MapEventHandler.get_system_static_info(solar_system_id)
%{
label: solar_system_name,
value: solar_system_id,
constellation_name: constellation_name,
region_name: region_name,
class_title: class_title,
system_static_info: system_static_info
}
end
defp can_update_system?(:locked, %{lock_system: false} = _user_permissions), do: false
defp can_update_system?(_key, _user_permissions), do: true

View File

@@ -24,7 +24,6 @@ defmodule WandererAppWeb.MapEventHandler do
@map_characters_ui_events [
"add_character",
"toggle_track",
"toggle_follow",
"hide_tracking"
]
@@ -39,10 +38,10 @@ defmodule WandererAppWeb.MapEventHandler do
@map_system_ui_events [
"add_hub",
"delete_hub",
"add_system",
"delete_systems",
"get_system_static_infos",
"manual_add_system",
"search_systems",
"get_system_static_infos",
"update_system_position",
"update_system_positions",
"update_system_name",

View File

@@ -102,6 +102,13 @@ defmodule WandererAppWeb.MapLive do
|> assign(:active_page, :map)
end
defp apply_action(socket, :add_system, _params) do
socket
|> assign(:active_page, :map)
|> assign(:page_title, "Add System")
|> assign(:add_system_form, to_form(%{"system_id" => nil}))
end
def character_item(assigns) do
~H"""
<div class="flex items-center gap-3">

View File

@@ -31,20 +31,74 @@
</.link>
</div>
<.modal
:if={@live_action in [:add_system] && not is_nil(assigns |> Map.get(:map_slug)) && @map_loaded?}
id="add-system-modal"
class="!w-[400px]"
title="Add System"
show
on_cancel={JS.patch(~p"/#{@map_slug}")}
>
<.form :let={f} for={@add_system_form} phx-submit="add_system">
<.live_select
label="Search system"
field={f[:system_id]}
update_min_len={2}
available_option_class="w-full text-sm"
debounce={200}
mode={:tags}
>
<:option :let={option}>
<div class="gap-1 w-full flex flex-align-center p-autocomplete-item text-sm">
<div class="eve-wh-type-color-c1 text-gray-400 w-8"><%= option.class_title %></div>
<div class="text-white w-16"><%= option.label %></div>
<div class="text-gray-600 w-20"><%= option.constellation_name %></div>
<div class="text-gray-600"><%= option.region_name %></div>
</div>
</:option>
</.live_select>
<div class="mt-2 bg-neutral text-neutral-content rounded-md p-1 text-xs w-full">
* Start search system. You should type at least 2 symbols.
</div>
<div class="modal-action mt-0">
<.button class="mt-2" type="submit">Add</.button>
</div>
</.form>
</.modal>
<.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")}
>
<.table
:if={not (assigns |> Map.get(:character_activity) |> is_nil())}
class="!max-h-[80vh] !overflow-y-auto"
id="activity-tbl"
rows={@character_activity.jumps}
>
<:col :let={activity} label="Character">
<.character_item character={activity.character} />
</:col>
<:col :let={activity} label="Passages">
<%= activity.count %>
</:col>
</.table>
</.modal>
<.modal
:if={assigns |> Map.get(:show_tracking?, false)}
id="map-tracking-modal"
title="Track and Follow Characters"
title="Track 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>
<:loading><span class="loading loading-dots loading-xs" /></:loading>
<:failed :let={reason}><%= reason %></:failed>
<.table
:if={characters}
@@ -53,7 +107,7 @@
rows={characters}
>
<:col :let={character} label="Track">
<label class="flex items-center gap-2 justify-center">
<label class="flex items-center gap-3">
<input
type="checkbox"
class="checkbox"
@@ -62,33 +116,16 @@
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 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 class="text-sm opacity-50"></div>
</div>
</div>
</label>
</:col>
</.table>
</.async_result>

View File

@@ -197,6 +197,7 @@ defmodule WandererAppWeb.Router do
live("/profile/deposit", ProfileLive, :deposit)
live("/profile/subscribe", ProfileLive, :subscribe)
live("/:slug/audit", MapAuditLive, :index)
live("/:slug/add-system", MapLive, :add_system)
live("/:slug", MapLive, :index)
end
end

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.32.2"
@version "1.30.2"
def project do
[

View File

@@ -1,21 +0,0 @@
defmodule WandererApp.Repo.Migrations.MigrateResources1 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_character_settings_v1) do
add :followed, :boolean, default: false
end
end
def down do
alter table(:map_character_settings_v1) do
remove :followed
end
end
end