Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
383e2e07bd
|
@@ -9,6 +9,8 @@ files:
|
|||||||
extraResources:
|
extraResources:
|
||||||
- from: .env
|
- from: .env
|
||||||
to: .env
|
to: .env
|
||||||
|
- from: resources/icons
|
||||||
|
to: icons
|
||||||
win:
|
win:
|
||||||
target:
|
target:
|
||||||
- portable
|
- portable
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rmtpocketwatcher",
|
"name": "rmtpocketwatcher",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "Real-time AUEC price tracking desktop application",
|
"description": "Real-time AUEC price tracking desktop application",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import { initDatabase, closeDatabase } from './database.js';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Set app name for notifications (must be before app ready)
|
||||||
|
app.setName('rmtPocketWatcher');
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
app.setAppUserModelId('com.lambdabanking.rmtpocketwatcher');
|
||||||
|
}
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
// In dev: __dirname = dist/main, so go up to electron-app root
|
// In dev: __dirname = dist/main, so go up to electron-app root
|
||||||
// In prod: __dirname = resources/app.asar/dist/main, .env should be in resources
|
// In prod: __dirname = resources/app.asar/dist/main, .env should be in resources
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export function App() {
|
|||||||
const [selectedRange, setSelectedRange] = useState('7d');
|
const [selectedRange, setSelectedRange] = useState('7d');
|
||||||
const [zoomState, setZoomState] = useState<ZoomState | null>(null);
|
const [zoomState, setZoomState] = useState<ZoomState | null>(null);
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [animateChart, setAnimateChart] = useState(true);
|
||||||
|
const [hoveredSeller, setHoveredSeller] = useState<string | null>(null);
|
||||||
|
|
||||||
// Price Alert State
|
// Price Alert State
|
||||||
const [alerts, setAlerts] = useState<PriceAlert[]>([]);
|
const [alerts, setAlerts] = useState<PriceAlert[]>([]);
|
||||||
@@ -262,6 +264,7 @@ export function App() {
|
|||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
const isZoomIn = e.deltaY < 0;
|
const isZoomIn = e.deltaY < 0;
|
||||||
const zoomFactor = isZoomIn ? 0.8 : 1.25; // Zoom in = smaller range, zoom out = larger range
|
const zoomFactor = isZoomIn ? 0.8 : 1.25; // Zoom in = smaller range, zoom out = larger range
|
||||||
@@ -334,11 +337,13 @@ export function App() {
|
|||||||
}, [fullChartData, zoomState, yAxisDomain]);
|
}, [fullChartData, zoomState, yAxisDomain]);
|
||||||
|
|
||||||
const handleRangeChange = (range: string) => {
|
const handleRangeChange = (range: string) => {
|
||||||
|
setAnimateChart(true);
|
||||||
setSelectedRange(range);
|
setSelectedRange(range);
|
||||||
setZoomState(null);
|
setZoomState(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetZoom = () => {
|
const resetZoom = () => {
|
||||||
|
setAnimateChart(false);
|
||||||
setZoomState(null);
|
setZoomState(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -415,6 +420,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
if (!fullChartData.length) return;
|
if (!fullChartData.length) return;
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
// Keep current X range, only adjust Y max
|
// Keep current X range, only adjust Y max
|
||||||
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
||||||
@@ -434,6 +440,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
if (!fullChartData.length) return;
|
if (!fullChartData.length) return;
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
// Keep current X range, only adjust Y max
|
// Keep current X range, only adjust Y max
|
||||||
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
||||||
@@ -453,6 +460,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleTimelineCompress = () => {
|
const handleTimelineCompress = () => {
|
||||||
if (!fullChartData.length) return;
|
if (!fullChartData.length) return;
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
||||||
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
|
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
|
||||||
@@ -491,6 +499,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleTimelineExpand = () => {
|
const handleTimelineExpand = () => {
|
||||||
if (!fullChartData.length) return;
|
if (!fullChartData.length) return;
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
|
||||||
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
|
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
|
||||||
@@ -542,6 +551,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!fullChartData.length) return;
|
if (!fullChartData.length) return;
|
||||||
|
setAnimateChart(false);
|
||||||
|
|
||||||
const position = parseFloat(e.target.value);
|
const position = parseFloat(e.target.value);
|
||||||
const dataXStart = fullChartData[0].timestamp;
|
const dataXStart = fullChartData[0].timestamp;
|
||||||
@@ -893,15 +903,59 @@ export function App() {
|
|||||||
allowDataOverflow={true}
|
allowDataOverflow={true}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: '#1a1f3a', border: '1px solid #2a2f4a', borderRadius: '4px' }}
|
content={({ active, payload }) => {
|
||||||
labelStyle={{ color: '#fff' }}
|
if (!active || !payload || payload.length === 0) return null;
|
||||||
labelFormatter={(label, payload) => {
|
const data = payload[0]?.payload;
|
||||||
if (payload && payload.length > 0 && payload[0].payload.fullTime) {
|
if (!data) return null;
|
||||||
return payload[0].payload.fullTime;
|
const seenNames = new Set<string>();
|
||||||
|
const validSellers = payload
|
||||||
|
.filter((p: any) => {
|
||||||
|
if (p.value === undefined || p.value === null || isNaN(p.value)) return false;
|
||||||
|
if (!p.name || String(p.name).trim() === '') return false;
|
||||||
|
if (p.stroke === 'transparent') return false;
|
||||||
|
// Deduplicate by name
|
||||||
|
if (seenNames.has(p.name)) return false;
|
||||||
|
seenNames.add(p.name);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a: any, b: any) => a.value - b.value);
|
||||||
|
if (validSellers.length === 0) return null;
|
||||||
|
|
||||||
|
// If hovering over a specific line, show that seller and others with same price
|
||||||
|
let displaySellers = validSellers;
|
||||||
|
if (hoveredSeller) {
|
||||||
|
const hoveredData = validSellers.find((s: any) => s.name === hoveredSeller);
|
||||||
|
if (hoveredData) {
|
||||||
|
const hoveredPrice = hoveredData.value;
|
||||||
|
// Show hovered seller and any with same price (within 0.0001 tolerance)
|
||||||
|
displaySellers = validSellers.filter((s: any) =>
|
||||||
|
s.name === hoveredSeller || Math.abs(s.value - hoveredPrice) < 0.0001
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return label;
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ backgroundColor: "#1a1f3a", border: "1px solid #50e3c2", borderRadius: "6px", padding: "12px", maxHeight: "300px", overflowY: "auto", minWidth: "200px" }}>
|
||||||
|
<div style={{ color: "#fff", fontWeight: "bold", marginBottom: "8px", borderBottom: "1px solid #2a2f4a", paddingBottom: "6px" }}>
|
||||||
|
{data.fullTime || data.time}
|
||||||
|
</div>
|
||||||
|
{!hoveredSeller && (
|
||||||
|
<div style={{ fontSize: "11px", color: "#888", marginBottom: "6px" }}>
|
||||||
|
{validSellers.length} seller{validSellers.length !== 1 ? "s" : ""} - Sorted by price
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{displaySellers.slice(0, hoveredSeller ? 20 : 10).map((seller: any, idx: number) => (
|
||||||
|
<div key={`${seller.name}-${idx}`} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 0", borderBottom: idx < displaySellers.length - 1 ? "1px solid #2a2f4a" : "none" }}>
|
||||||
|
<span style={{ color: seller.color, fontSize: "12px", fontWeight: seller.name === hoveredSeller ? "bold" : "normal" }}>{seller.name}</span>
|
||||||
|
<span style={{ color: "#50e3c2", fontWeight: "bold", fontSize: "12px", marginLeft: "10px" }}>${Number(seller.value).toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!hoveredSeller && validSellers.length > 10 && (
|
||||||
|
<div style={{ color: "#888", fontSize: "11px", marginTop: "6px", textAlign: "center" }}>+{validSellers.length - 10} more sellers</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
formatter={(value: any, name: string) => [`$${Number(value).toFixed(9)}`, name]}
|
|
||||||
wrapperStyle={{ zIndex: 1000 }}
|
wrapperStyle={{ zIndex: 1000 }}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
@@ -914,15 +968,37 @@ export function App() {
|
|||||||
}}
|
}}
|
||||||
iconType="line"
|
iconType="line"
|
||||||
/>
|
/>
|
||||||
|
{/* Invisible wider lines for better hover detection */}
|
||||||
|
{sellers.map((seller) => (
|
||||||
|
<Line
|
||||||
|
key={`${seller}-hitarea`}
|
||||||
|
type="linear"
|
||||||
|
dataKey={seller}
|
||||||
|
stroke="transparent"
|
||||||
|
strokeWidth={15}
|
||||||
|
dot={false}
|
||||||
|
connectNulls
|
||||||
|
isAnimationActive={false}
|
||||||
|
onMouseEnter={() => setHoveredSeller(seller)}
|
||||||
|
onMouseLeave={() => setHoveredSeller(null)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
legendType="none"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* Visible lines */}
|
||||||
{sellers.map((seller) => (
|
{sellers.map((seller) => (
|
||||||
<Line
|
<Line
|
||||||
key={seller}
|
key={seller}
|
||||||
type="monotone"
|
type="linear"
|
||||||
dataKey={seller}
|
dataKey={seller}
|
||||||
stroke={COLORS[sellers.indexOf(seller) % COLORS.length]}
|
stroke={COLORS[sellers.indexOf(seller) % COLORS.length]}
|
||||||
strokeWidth={2}
|
strokeWidth={hoveredSeller === seller ? 4 : 2}
|
||||||
dot={false}
|
dot={false}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
connectNulls
|
connectNulls
|
||||||
|
isAnimationActive={animateChart}
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
legendType="line"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export function TitleBar() {
|
|||||||
WebkitAppRegion: 'drag',
|
WebkitAppRegion: 'drag',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
padding: '0 15px',
|
padding: '0 15px',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1000,
|
||||||
} as any}
|
} as any}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user