8 Commits
v ... main

Author SHA1 Message Date
383e2e07bd Pierre smells like cheese
All checks were successful
Windows Release / build-windows (push) Successful in 2m52s
2025-12-04 17:41:43 -05:00
c708fedca3 Sound and more view controls
All checks were successful
Windows Release / build-windows (push) Successful in 2m46s
2025-12-04 16:09:20 -05:00
690015aa40 update .env writing 2025-12-04 15:04:28 -05:00
6e04dc1c74 CJ kill my Self 2025-12-04 14:57:10 -05:00
34b2aed773 Helper 2025-12-04 14:52:39 -05:00
a967176454 hurr 2025-12-04 14:48:36 -05:00
56bc506aae Piss and cum
Some checks failed
Windows Release / build-windows (push) Failing after 2m36s
2025-12-04 14:45:28 -05:00
fd36c61f8f Penits
Some checks failed
Windows Release / build-windows (push) Failing after 1m3s
2025-12-04 14:43:37 -05:00
12 changed files with 338 additions and 44 deletions

View File

@@ -25,10 +25,14 @@ jobs:
- name: Create production .env file - name: Create production .env file
working-directory: electron-app working-directory: electron-app
run: | env:
echo "WS_URL=${{ secrets.WS_URL }}" > .env WS_URL: ${{ secrets.WS_URL }}
echo "API_URL=${{ secrets.API_URL }}" >> .env API_URL: ${{ secrets.API_URL }}
echo "NODE_ENV=production" >> .env run: node scripts/create-env.cjs
- name: Verify .env file
working-directory: electron-app
run: type .env
- name: Build TypeScript - name: Build TypeScript
working-directory: electron-app working-directory: electron-app
@@ -43,9 +47,7 @@ jobs:
- name: Get version from package.json - name: Get version from package.json
id: version id: version
working-directory: electron-app working-directory: electron-app
run: | run: node scripts/get-version.cjs
$version = (Get-Content package.json | ConvertFrom-Json).version
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Create Release and Upload exe - name: Create Release and Upload exe
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

View File

@@ -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

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; media-src 'self' data:; img-src 'self' data:">
<title>rmtPocketWatcher</title> <title>rmtPocketWatcher</title>
<style> <style>
* { * {

View File

@@ -1,6 +1,6 @@
{ {
"name": "rmtpocketwatcher", "name": "rmtpocketwatcher",
"version": "1.0.2", "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",

Binary file not shown.

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
const envPath = path.join(__dirname, '..', '.env');
const content = `WS_URL=${process.env.WS_URL || ''}
API_URL=${process.env.API_URL || ''}
NODE_ENV=production
`;
fs.writeFileSync(envPath, content, 'utf8');
console.log('.env file created at:', envPath);
console.log('Contents:');
console.log(content);

View File

@@ -0,0 +1,12 @@
const fs = require('fs');
const path = require('path');
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, `VERSION=${pkg.version}\n`);
}
console.log(pkg.version);

View File

@@ -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

View File

@@ -13,29 +13,40 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
wsClient = new WebSocketClient(wsUrl); wsClient = new WebSocketClient(wsUrl);
} }
// Helper to safely send to renderer (window may be destroyed or hidden)
const safeSend = (channel: string, ...args: any[]) => {
try {
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
mainWindow.webContents.send(channel, ...args);
}
} catch (error) {
// Window was destroyed, ignore
}
};
// Forward WebSocket events to renderer // Forward WebSocket events to renderer
wsClient.on('connected', () => { wsClient.on('connected', () => {
mainWindow.webContents.send('ws:connected'); safeSend('ws:connected');
}); });
wsClient.on('disconnected', () => { wsClient.on('disconnected', () => {
mainWindow.webContents.send('ws:disconnected'); safeSend('ws:disconnected');
}); });
wsClient.on('priceUpdate', (data: PriceIndex) => { wsClient.on('priceUpdate', (data: PriceIndex) => {
mainWindow.webContents.send('ws:priceUpdate', data); safeSend('ws:priceUpdate', data);
}); });
wsClient.on('historyData', (data: any) => { wsClient.on('historyData', (data: any) => {
mainWindow.webContents.send('ws:historyData', data); safeSend('ws:historyData', data);
}); });
wsClient.on('error', (error: Error) => { wsClient.on('error', (error: Error) => {
mainWindow.webContents.send('ws:error', error.message); safeSend('ws:error', error.message);
}); });
wsClient.on('maxReconnectAttemptsReached', () => { wsClient.on('maxReconnectAttemptsReached', () => {
mainWindow.webContents.send('ws:maxReconnectAttemptsReached'); safeSend('ws:maxReconnectAttemptsReached');
}); });
// Remove existing handlers // Remove existing handlers

View File

@@ -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,22 +420,19 @@ 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
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;
const currentYMax = zoomState?.yMax ?? yAxisDomain[1]; const currentYMax = zoomState?.yMax ?? yAxisDomain[1];
const xRange = currentXEnd - currentXStart; // Y-axis: decrease max to zoom in (show less range)
const newXRange = xRange * 0.8;
const xCenter = (currentXStart + currentXEnd) / 2;
// Y-axis: zoom from 0
const newYMax = currentYMax * 0.8; const newYMax = currentYMax * 0.8;
setZoomState({ setZoomState({
xStart: xCenter - newXRange / 2, xStart: currentXStart,
xEnd: xCenter + newXRange / 2, xEnd: currentXEnd,
yMin: 0, yMin: 0,
yMax: newYMax, yMax: newYMax,
}); });
@@ -438,30 +440,87 @@ 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
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
const currentYMax = zoomState?.yMax ?? yAxisDomain[1];
// Y-axis: increase max to zoom out (show more range)
const newYMax = currentYMax * 1.25;
setZoomState({
xStart: currentXStart,
xEnd: currentXEnd,
yMin: 0,
yMax: newYMax,
});
};
const handleTimelineCompress = () => {
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;
const currentYMax = zoomState?.yMax ?? yAxisDomain[1]; const currentYMax = zoomState?.yMax ?? yAxisDomain[1];
const xRange = currentXEnd - currentXStart; const xRange = currentXEnd - currentXStart;
const newXRange = xRange * 1.25; const newXRange = xRange * 0.8; // Compress = show less time
const xCenter = (currentXStart + currentXEnd) / 2; const xCenter = (currentXStart + currentXEnd) / 2;
// Constrain to data bounds
const dataXStart = fullChartData[0].timestamp;
const dataXEnd = fullChartData[fullChartData.length - 1].timestamp;
let newXStart = xCenter - newXRange / 2;
let newXEnd = xCenter + newXRange / 2;
// Ensure we stay within data bounds
if (newXStart < dataXStart) {
newXEnd += dataXStart - newXStart;
newXStart = dataXStart;
}
if (newXEnd > dataXEnd) {
newXStart -= newXEnd - dataXEnd;
newXEnd = dataXEnd;
}
newXStart = Math.max(dataXStart, newXStart);
newXEnd = Math.min(dataXEnd, newXEnd);
setZoomState({
xStart: newXStart,
xEnd: newXEnd,
yMin: 0,
yMax: currentYMax,
});
};
const handleTimelineExpand = () => {
if (!fullChartData.length) return;
setAnimateChart(false);
const currentXStart = zoomState?.xStart ?? fullChartData[0].timestamp;
const currentXEnd = zoomState?.xEnd ?? fullChartData[fullChartData.length - 1].timestamp;
const currentYMax = zoomState?.yMax ?? yAxisDomain[1];
const xRange = currentXEnd - currentXStart;
const newXRange = xRange * 1.25; // Expand = show more time
const xCenter = (currentXStart + currentXEnd) / 2;
// Constrain to data bounds
const dataXStart = fullChartData[0].timestamp; const dataXStart = fullChartData[0].timestamp;
const dataXEnd = fullChartData[fullChartData.length - 1].timestamp; const dataXEnd = fullChartData[fullChartData.length - 1].timestamp;
const newXStart = Math.max(dataXStart, xCenter - newXRange / 2); const newXStart = Math.max(dataXStart, xCenter - newXRange / 2);
const newXEnd = Math.min(dataXEnd, xCenter + newXRange / 2); const newXEnd = Math.min(dataXEnd, xCenter + newXRange / 2);
// Y-axis: zoom from 0
const newYMax = currentYMax * 1.25;
setZoomState({ setZoomState({
xStart: newXStart, xStart: newXStart,
xEnd: newXEnd, xEnd: newXEnd,
yMin: 0, yMin: 0,
yMax: newYMax, yMax: currentYMax,
}); });
}; };
@@ -474,6 +533,64 @@ export function App() {
return fullXRange / currentXRange; return fullXRange / currentXRange;
}; };
// Timeline slider position (0-100)
const sliderPosition = useMemo(() => {
if (!fullChartData.length) return 50;
const dataXStart = fullChartData[0].timestamp;
const dataXEnd = fullChartData[fullChartData.length - 1].timestamp;
const fullRange = dataXEnd - dataXStart;
if (!zoomState || fullRange === 0) return 50;
const viewCenter = (zoomState.xStart + zoomState.xEnd) / 2;
const position = ((viewCenter - dataXStart) / fullRange) * 100;
return Math.max(0, Math.min(100, position));
}, [fullChartData, zoomState]);
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!fullChartData.length) return;
setAnimateChart(false);
const position = parseFloat(e.target.value);
const dataXStart = fullChartData[0].timestamp;
const dataXEnd = fullChartData[fullChartData.length - 1].timestamp;
const fullRange = dataXEnd - dataXStart;
// Calculate new center based on slider position
const newCenter = dataXStart + (position / 100) * fullRange;
// Keep current view width or use full range if no zoom
const currentXStart = zoomState?.xStart ?? dataXStart;
const currentXEnd = zoomState?.xEnd ?? dataXEnd;
const currentYMax = zoomState?.yMax ?? yAxisDomain[1];
const viewWidth = currentXEnd - currentXStart;
// Calculate new bounds centered on slider position
let newXStart = newCenter - viewWidth / 2;
let newXEnd = newCenter + viewWidth / 2;
// Constrain to data bounds
if (newXStart < dataXStart) {
newXEnd += dataXStart - newXStart;
newXStart = dataXStart;
}
if (newXEnd > dataXEnd) {
newXStart -= newXEnd - dataXEnd;
newXEnd = dataXEnd;
}
newXStart = Math.max(dataXStart, newXStart);
newXEnd = Math.min(dataXEnd, newXEnd);
setZoomState({
xStart: newXStart,
xEnd: newXEnd,
yMin: 0,
yMax: currentYMax,
});
};
return ( return (
<div style={{ <div style={{
fontFamily: 'system-ui', fontFamily: 'system-ui',
@@ -716,6 +833,38 @@ export function App() {
> >
Reset (×{getZoomLevel().toFixed(1)}) Reset (×{getZoomLevel().toFixed(1)})
</button> </button>
<button
onClick={handleTimelineExpand}
style={{
padding: '6px 12px',
backgroundColor: '#2a2f4a',
color: '#fff',
border: '1px solid #50e3c2',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
}}
title="Expand Timeline"
>
&lt;
</button>
<button
onClick={handleTimelineCompress}
style={{
padding: '6px 12px',
backgroundColor: '#2a2f4a',
color: '#fff',
border: '1px solid #50e3c2',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
}}
title="Compress Timeline"
>
&gt;
</button>
</div> </div>
<div style={{ color: '#888', fontSize: '11px', fontStyle: 'italic' }}> <div style={{ color: '#888', fontSize: '11px', fontStyle: 'italic' }}>
@@ -753,16 +902,60 @@ export function App() {
domain={yAxisDomain} domain={yAxisDomain}
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
@@ -775,20 +968,73 @@ 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>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
{/* Timeline Slider */}
<div style={{
marginTop: '15px',
padding: '10px 15px',
backgroundColor: '#0f1329',
borderRadius: '4px',
border: '1px solid #2a2f4a',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<span style={{ color: '#888', fontSize: '12px', minWidth: '60px' }}>
{fullChartData.length > 0 ? new Date(fullChartData[0].timestamp).toLocaleDateString() : ''}
</span>
<input
type="range"
min="0"
max="100"
value={sliderPosition}
onChange={handleSliderChange}
style={{
flex: 1,
height: '8px',
cursor: 'pointer',
accentColor: '#50e3c2',
}}
/>
<span style={{ color: '#888', fontSize: '12px', minWidth: '60px', textAlign: 'right' }}>
{fullChartData.length > 0 ? new Date(fullChartData[fullChartData.length - 1].timestamp).toLocaleDateString() : ''}
</span>
</div>
</div>
</div> </div>
) : ( ) : (
<div style={{ textAlign: 'center', padding: '40px', color: '#ff6b9d' }}> <div style={{ textAlign: 'center', padding: '40px', color: '#ff6b9d' }}>
@@ -854,7 +1100,7 @@ export function App() {
<th style={{ textAlign: 'right', padding: '12px', color: '#888', fontWeight: 'normal' }}>Price/1M AUEC</th> <th style={{ textAlign: 'right', padding: '12px', color: '#888', fontWeight: 'normal' }}>Price/1M AUEC</th>
{customAuecAmount && ( {customAuecAmount && (
<th style={{ textAlign: 'right', padding: '12px', color: '#888', fontWeight: 'normal' }}> <th style={{ textAlign: 'right', padding: '12px', color: '#888', fontWeight: 'normal' }}>
Price for {(customAuecAmount / 1000000).toLocaleString()}M AUEC Price for {customAuecAmount.toLocaleString()} AUEC
</th> </th>
)} )}
</tr> </tr>
@@ -928,10 +1174,7 @@ export function App() {
)} )}
{/* Alert Audio */} {/* Alert Audio */}
<audio <audio ref={alertAudioRef} src="./notifcation.mp3" />
ref={alertAudioRef}
src="data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIGGS57OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6OyrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWQ="
/>
</div> </div>
); );
} }

View File

@@ -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' }}>

View File

@@ -5,6 +5,7 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
root: '.', root: '.',
base: './', // Use relative paths for Electron base: './', // Use relative paths for Electron
publicDir: 'public',
build: { build: {
outDir: 'dist/renderer', outDir: 'dist/renderer',
emptyOutDir: true, emptyOutDir: true,