Compare commits
45 Commits
0.0.0.0.0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
383e2e07bd
|
|||
|
c708fedca3
|
|||
|
690015aa40
|
|||
|
6e04dc1c74
|
|||
|
34b2aed773
|
|||
|
a967176454
|
|||
|
56bc506aae
|
|||
|
fd36c61f8f
|
|||
|
f017b1322c
|
|||
|
3c293dbca7
|
|||
|
d5ead6deaf
|
|||
|
4488008494
|
|||
|
f0190b3d20
|
|||
|
1b6a90ce8f
|
|||
|
507fa7d492
|
|||
|
29d4068e29
|
|||
|
ab663e7d2b
|
|||
|
4a1b5bfc24
|
|||
|
54d8c96c7f
|
|||
|
483d1286df
|
|||
|
6716891197
|
|||
|
f293bfc8cd
|
|||
|
ec15c7f74e
|
|||
|
ab23debac5
|
|||
|
4f78e77579
|
|||
|
9466ee7d9d
|
|||
|
3382a52e83
|
|||
|
afc2ebcfb5
|
|||
|
9e1fa511b1
|
|||
|
30fafb6630
|
|||
|
4c720a2923
|
|||
|
a2725c114a
|
|||
|
d432a8cac3
|
|||
|
63f1ede907
|
|||
|
a3cc75e506
|
|||
|
be86c8c224
|
|||
|
53e3a390ed
|
|||
|
9372c0fee5
|
|||
|
f81378d23b
|
|||
|
316745f716
|
|||
|
65fbe77baf
|
|||
|
2056e68c53
|
|||
|
4d26048e52
|
|||
|
73b9cfefa9
|
|||
|
828ad8d97f
|
55
.gitea/workflows/build-dev.yml
Normal file
55
.gitea/workflows/build-dev.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Manual Dev Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-dev:
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Node.js
|
||||||
|
run: node -v
|
||||||
|
|
||||||
|
- name: Install electron-app dependencies
|
||||||
|
working-directory: electron-app
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build TypeScript (main + preload)
|
||||||
|
working-directory: electron-app
|
||||||
|
run: npm run build:main
|
||||||
|
|
||||||
|
- name: Build Renderer (Vite)
|
||||||
|
working-directory: electron-app
|
||||||
|
run: npm run build:renderer
|
||||||
|
|
||||||
|
- name: List dist directory
|
||||||
|
working-directory: electron-app
|
||||||
|
run: |
|
||||||
|
Write-Host "=== Dist Directory Structure ==="
|
||||||
|
Get-ChildItem -Recurse dist | Select-Object FullName
|
||||||
|
|
||||||
|
- name: Package with electron-builder (unpacked only)
|
||||||
|
working-directory: electron-app
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
|
run: npx electron-builder --win --dir
|
||||||
|
|
||||||
|
- name: List release directory
|
||||||
|
working-directory: electron-app
|
||||||
|
run: |
|
||||||
|
Write-Host "=== Release Directory ==="
|
||||||
|
if (Test-Path "release") {
|
||||||
|
Get-ChildItem -Recurse release | Select-Object FullName, Length
|
||||||
|
} else {
|
||||||
|
Write-Host "Release directory not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload unpacked build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rmtPocketWatcher-Windows-Unpacked
|
||||||
|
path: electron-app/release/win-unpacked/
|
||||||
|
retention-days: 7
|
||||||
@@ -1,96 +1,61 @@
|
|||||||
name: Build and Release
|
name: Windows Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
paths:
|
||||||
- 'v*'
|
- 'electron-app/package.json'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
# Node 20 should be preinstalled on the Windows host runner; skipping setup-node avoids 7zip download issues.
|
||||||
uses: actions/setup-node@v4
|
- name: Verify Node.js
|
||||||
with:
|
run: node -v
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install electron-app dependencies
|
- name: Install electron-app dependencies
|
||||||
working-directory: electron-app
|
working-directory: electron-app
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build and package for Windows
|
- name: Create production .env file
|
||||||
working-directory: electron-app
|
working-directory: electron-app
|
||||||
run: npm run electron:build -- --win
|
env:
|
||||||
|
WS_URL: ${{ secrets.WS_URL }}
|
||||||
|
API_URL: ${{ secrets.API_URL }}
|
||||||
|
run: node scripts/create-env.cjs
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
- name: Verify .env file
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-build
|
|
||||||
path: electron-app/release/*.exe
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install electron-app dependencies
|
|
||||||
working-directory: electron-app
|
working-directory: electron-app
|
||||||
run: npm ci
|
run: type .env
|
||||||
|
|
||||||
- name: Build and package for Linux
|
- name: Build TypeScript
|
||||||
working-directory: electron-app
|
working-directory: electron-app
|
||||||
run: npm run electron:build -- --linux
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload Linux AppImage
|
- name: Build Windows portable executable (skip signing)
|
||||||
uses: actions/upload-artifact@v4
|
working-directory: electron-app
|
||||||
with:
|
env:
|
||||||
name: linux-appimage
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
path: electron-app/release/*.AppImage
|
run: npx electron-builder --win portable --config electron-builder.yml
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Upload Linux deb
|
- name: Get version from package.json
|
||||||
uses: actions/upload-artifact@v4
|
id: version
|
||||||
with:
|
working-directory: electron-app
|
||||||
name: linux-deb
|
run: node scripts/get-version.cjs
|
||||||
path: electron-app/release/*.deb
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
create-release:
|
- name: Create Release and Upload exe
|
||||||
needs: [build-windows, build-linux]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./artifacts
|
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R ./artifacts
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: |
|
tag_name: v${{ steps.version.outputs.VERSION }}
|
||||||
artifacts/windows-build/*.exe
|
name: rmtPocketWatcher v${{ steps.version.outputs.VERSION }}
|
||||||
artifacts/linux-appimage/*.AppImage
|
|
||||||
artifacts/linux-deb/*.deb
|
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
generate_release_notes: true
|
files: electron-app/release/rmtPocketWatcher-*.exe
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "price_index" ALTER COLUMN "lowest_price" SET DATA TYPE DECIMAL(13,9);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "raw_vendor_prices" ALTER COLUMN "usd_per_million" SET DATA TYPE DECIMAL(13,9);
|
||||||
@@ -17,7 +17,7 @@ model VendorPrice {
|
|||||||
sellerName String? @map("seller_name")
|
sellerName String? @map("seller_name")
|
||||||
usdPrice Decimal @map("usd_price") @db.Decimal(12, 2)
|
usdPrice Decimal @map("usd_price") @db.Decimal(12, 2)
|
||||||
auecAmount BigInt @map("auec_amount")
|
auecAmount BigInt @map("auec_amount")
|
||||||
usdPerMillion Decimal @map("usd_per_million") @db.Decimal(12, 8)
|
usdPerMillion Decimal @map("usd_per_million") @db.Decimal(13, 9)
|
||||||
deliveryTime String? @map("delivery_time")
|
deliveryTime String? @map("delivery_time")
|
||||||
url String
|
url String
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ model VendorPrice {
|
|||||||
model PriceIndex {
|
model PriceIndex {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
timestamp DateTime @default(now()) @db.Timestamptz(3)
|
timestamp DateTime @default(now()) @db.Timestamptz(3)
|
||||||
lowestPrice Decimal @map("lowest_price") @db.Decimal(12, 8)
|
lowestPrice Decimal @map("lowest_price") @db.Decimal(13, 9)
|
||||||
vendor String
|
vendor String
|
||||||
sellerName String? @map("seller_name")
|
sellerName String? @map("seller_name")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# WebSocket connection URL
|
# WebSocket connection URL
|
||||||
WS_URL=ws://localhost:3000/ws
|
WS_URL=ws://localhost:3000/ws
|
||||||
|
|
||||||
|
# API URL
|
||||||
|
API_URL=http://localhost:3000
|
||||||
|
|
||||||
# Development mode
|
# Development mode
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|||||||
4
electron-app/.gitignore
vendored
4
electron-app/.gitignore
vendored
@@ -1,12 +1,12 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
dist-electron/
|
dist-electron/
|
||||||
out/
|
out/
|
||||||
build/
|
build/*
|
||||||
|
!build/icon.ico
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
|||||||
BIN
electron-app/build/icon.ico
Normal file
BIN
electron-app/build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
27
electron-app/electron-builder.yml
Normal file
27
electron-app/electron-builder.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
appId: com.lambdabanking.rmtpocketwatcher
|
||||||
|
productName: rmtPocketWatcher
|
||||||
|
directories:
|
||||||
|
output: release
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- dist/**/*
|
||||||
|
- package.json
|
||||||
|
extraResources:
|
||||||
|
- from: .env
|
||||||
|
to: .env
|
||||||
|
- from: resources/icons
|
||||||
|
to: icons
|
||||||
|
win:
|
||||||
|
target:
|
||||||
|
- portable
|
||||||
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
|
icon: null
|
||||||
|
mac:
|
||||||
|
target:
|
||||||
|
- dmg
|
||||||
|
identity: null
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- deb
|
||||||
|
category: Finance
|
||||||
@@ -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>
|
||||||
* {
|
* {
|
||||||
|
|||||||
7932
electron-app/package-lock.json
generated
Normal file
7932
electron-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "rmtpocketwatcher",
|
"name": "rmtpocketwatcher",
|
||||||
"version": "1.0.0",
|
"version": "1.0.5",
|
||||||
"description": "Real-time AUEC price tracking desktop application",
|
"description": "Real-time AUEC price tracking desktop application",
|
||||||
|
"type": "module",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build:main": "tsc --project tsconfig.main.json && tsc --project tsconfig.preload.json",
|
"build:main": "npx tsc --project tsconfig.main.json && npx tsc --project tsconfig.preload.json",
|
||||||
"build:renderer": "vite build",
|
"build:renderer": "npx vite build",
|
||||||
"build": "npm run build:main && npm run build:renderer",
|
"build": "npm run build:main && npm run build:renderer",
|
||||||
|
"build:portable": "npm run build && npx electron-builder --win portable",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"clean:dev": "npm run clean && npm run build:main && concurrently \"npm run dev\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron dist/main/index.js\"",
|
||||||
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron dist/main/index.js\"",
|
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron dist/main/index.js\"",
|
||||||
"electron:build": "npm run build && electron-builder",
|
"electron:build": "npm run build && electron-builder",
|
||||||
"publish": "npm run build && electron-builder --publish always",
|
"publish": "npm run build && electron-builder --publish always",
|
||||||
"rebuild": "electron-rebuild",
|
"rebuild": "electron-rebuild",
|
||||||
"postinstall": "electron-rebuild",
|
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.5.0",
|
"better-sqlite3": "^12.5.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"electron": "^30.0.0",
|
|
||||||
"electron-updater": "^6.1.0",
|
"electron-updater": "^6.1.0",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
@@ -31,44 +33,14 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"concurrently": "^8.2.0",
|
"concurrently": "^8.2.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
|
"electron": "^30.0.0",
|
||||||
"electron-builder": "^24.9.0",
|
"electron-builder": "^24.9.0",
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"wait-on": "^7.2.0"
|
"wait-on": "^7.2.0"
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "com.lambdabanking.rmtpocketwatcher",
|
|
||||||
"productName": "rmtPocketWatcher",
|
|
||||||
"directories": {
|
|
||||||
"output": "release"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**/*",
|
|
||||||
"package.json"
|
|
||||||
],
|
|
||||||
"publish": {
|
|
||||||
"provider": "generic",
|
|
||||||
"url": "${env.RELEASE_URL}"
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": [
|
|
||||||
"nsis"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"target": [
|
|
||||||
"dmg"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"AppImage",
|
|
||||||
"deb"
|
|
||||||
],
|
|
||||||
"category": "Finance"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
electron-app/public/notifcation.mp3
Normal file
BIN
electron-app/public/notifcation.mp3
Normal file
Binary file not shown.
BIN
electron-app/resources/icons/icon.ico
Normal file
BIN
electron-app/resources/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
electron-app/resources/icons/logo.png
Normal file
BIN
electron-app/resources/icons/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
14
electron-app/scripts/create-env.cjs
Normal file
14
electron-app/scripts/create-env.cjs
Normal 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);
|
||||||
12
electron-app/scripts/get-version.cjs
Normal file
12
electron-app/scripts/get-version.cjs
Normal 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);
|
||||||
@@ -33,6 +33,14 @@ export function initDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Create settings table for custom AUEC amount
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
console.log('Database initialized at:', dbPath);
|
console.log('Database initialized at:', dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +88,26 @@ export function deleteAlert(id: string): void {
|
|||||||
stmt.run(id);
|
stmt.run(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCustomAuecAmount(): number | null {
|
||||||
|
if (!db) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
const stmt = db.prepare('SELECT value FROM settings WHERE key = ?');
|
||||||
|
const row = stmt.get('customAuecAmount') as any;
|
||||||
|
|
||||||
|
return row ? parseFloat(row.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCustomAuecAmount(amount: number): void {
|
||||||
|
if (!db) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO settings (key, value)
|
||||||
|
VALUES (?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run('customAuecAmount', amount.toString());
|
||||||
|
}
|
||||||
|
|
||||||
export function closeDatabase(): void {
|
export function closeDatabase(): void {
|
||||||
if (db) {
|
if (db) {
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron';
|
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import { setupIpcHandlers, cleanupIpcHandlers } from './ipc-handlers';
|
import { setupIpcHandlers, cleanupIpcHandlers } from './ipc-handlers.js';
|
||||||
import { initDatabase, closeDatabase } from './database';
|
import { initDatabase, closeDatabase } from './database.js';
|
||||||
|
|
||||||
|
// ES module equivalent of __dirname
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
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
|
||||||
@@ -18,13 +29,22 @@ console.log('API_URL:', process.env.API_URL);
|
|||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
function getIconPath(): string {
|
||||||
|
if (isDev) {
|
||||||
|
return process.platform === 'win32'
|
||||||
|
? path.join(__dirname, '../../resources/icons/icon.ico')
|
||||||
|
: path.join(__dirname, '../../resources/icons/logo.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.platform === 'win32'
|
||||||
|
? path.join(process.resourcesPath, 'icons', 'icon.ico')
|
||||||
|
: path.join(process.resourcesPath, 'icons', 'logo.png');
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// In dev: __dirname = dist/main, logo is at root
|
const iconPath = getIconPath();
|
||||||
// In prod: __dirname = resources/app.asar/dist/main
|
|
||||||
const iconPath = process.env.NODE_ENV === 'development'
|
|
||||||
? path.join(__dirname, '../../logo.png')
|
|
||||||
: path.join(__dirname, '../assets/logo.png');
|
|
||||||
|
|
||||||
console.log('Window icon path:', iconPath);
|
console.log('Window icon path:', iconPath);
|
||||||
|
|
||||||
@@ -36,11 +56,13 @@ function createWindow(): void {
|
|||||||
frame: false,
|
frame: false,
|
||||||
backgroundColor: '#0a0e27',
|
backgroundColor: '#0a0e27',
|
||||||
icon: iconPath,
|
icon: iconPath,
|
||||||
|
show: false, // Don't show until loaded
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
devTools: true, // Enable dev tools in production for debugging
|
||||||
},
|
},
|
||||||
title: 'rmtPocketWatcher',
|
title: 'rmtPocketWatcher',
|
||||||
});
|
});
|
||||||
@@ -49,13 +71,27 @@ function createWindow(): void {
|
|||||||
setupIpcHandlers(mainWindow);
|
setupIpcHandlers(mainWindow);
|
||||||
|
|
||||||
// Load the app
|
// Load the app
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (isDev) {
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
const rendererPath = path.join(__dirname, '../renderer/index.html');
|
||||||
|
console.log('Loading renderer from:', rendererPath);
|
||||||
|
mainWindow.loadFile(rendererPath).catch(err => {
|
||||||
|
console.error('Failed to load renderer:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show window and open dev tools to see errors
|
||||||
|
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription) => {
|
||||||
|
console.error('Failed to load:', errorCode, errorDescription);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
|
console.log('Window loaded successfully');
|
||||||
|
mainWindow?.show();
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
cleanupIpcHandlers();
|
cleanupIpcHandlers();
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
@@ -65,13 +101,14 @@ function createWindow(): void {
|
|||||||
function createTray(): void {
|
function createTray(): void {
|
||||||
// In dev: __dirname = dist/main, logo is at root
|
// In dev: __dirname = dist/main, logo is at root
|
||||||
// In prod: __dirname = resources/app.asar/dist/main
|
// In prod: __dirname = resources/app.asar/dist/main
|
||||||
const iconPath = process.env.NODE_ENV === 'development'
|
const iconPath = getIconPath();
|
||||||
? path.join(__dirname, '../../logo.png')
|
|
||||||
: path.join(__dirname, '../assets/logo.png');
|
|
||||||
|
|
||||||
console.log('Tray icon path:', iconPath);
|
console.log('Tray icon path:', iconPath);
|
||||||
|
|
||||||
const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
|
let icon = nativeImage.createFromPath(iconPath);
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
icon = icon.resize({ width: 16, height: 16 });
|
||||||
|
}
|
||||||
|
|
||||||
tray = new Tray(icon);
|
tray = new Tray(icon);
|
||||||
|
|
||||||
@@ -85,6 +122,22 @@ function createTray(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Open DevTools',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reload',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ipcMain, BrowserWindow } from 'electron';
|
import { ipcMain, BrowserWindow } from 'electron';
|
||||||
import { WebSocketClient } from './websocket-client';
|
import { WebSocketClient } from './websocket-client.js';
|
||||||
import { PriceIndex } from '../shared/types';
|
import { PriceIndex } from '../shared/types.js';
|
||||||
import * as db from './database';
|
import * as db from './database.js';
|
||||||
|
|
||||||
let wsClient: WebSocketClient | null = null;
|
let wsClient: WebSocketClient | null = null;
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -53,6 +64,8 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
|||||||
try { ipcMain.removeHandler('alerts:add'); } catch (e) { /* ignore */ }
|
try { ipcMain.removeHandler('alerts:add'); } catch (e) { /* ignore */ }
|
||||||
try { ipcMain.removeHandler('alerts:update'); } catch (e) { /* ignore */ }
|
try { ipcMain.removeHandler('alerts:update'); } catch (e) { /* ignore */ }
|
||||||
try { ipcMain.removeHandler('alerts:delete'); } catch (e) { /* ignore */ }
|
try { ipcMain.removeHandler('alerts:delete'); } catch (e) { /* ignore */ }
|
||||||
|
try { ipcMain.removeHandler('settings:getCustomAuecAmount'); } catch (e) { /* ignore */ }
|
||||||
|
try { ipcMain.removeHandler('settings:setCustomAuecAmount'); } catch (e) { /* ignore */ }
|
||||||
|
|
||||||
// IPC handlers for renderer requests
|
// IPC handlers for renderer requests
|
||||||
ipcMain.handle('ws:connect', async () => {
|
ipcMain.handle('ws:connect', async () => {
|
||||||
@@ -155,6 +168,26 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom AUEC amount handlers
|
||||||
|
ipcMain.handle('settings:getCustomAuecAmount', async () => {
|
||||||
|
try {
|
||||||
|
return db.getCustomAuecAmount();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get custom AUEC amount:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('settings:setCustomAuecAmount', async (_event, amount: number) => {
|
||||||
|
try {
|
||||||
|
db.setCustomAuecAmount(amount);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set custom AUEC amount:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-connect on startup
|
// Auto-connect on startup
|
||||||
wsClient.connect();
|
wsClient.connect();
|
||||||
}
|
}
|
||||||
@@ -181,6 +214,8 @@ export function cleanupIpcHandlers(): void {
|
|||||||
ipcMain.removeHandler('alerts:add');
|
ipcMain.removeHandler('alerts:add');
|
||||||
ipcMain.removeHandler('alerts:update');
|
ipcMain.removeHandler('alerts:update');
|
||||||
ipcMain.removeHandler('alerts:delete');
|
ipcMain.removeHandler('alerts:delete');
|
||||||
|
ipcMain.removeHandler('settings:getCustomAuecAmount');
|
||||||
|
ipcMain.removeHandler('settings:setCustomAuecAmount');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handlers may not exist, ignore
|
// Handlers may not exist, ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { PriceIndex, WebSocketMessage, HistoricalData } from '../shared/types';
|
import { PriceIndex, WebSocketMessage, HistoricalData } from '../shared/types.js';
|
||||||
|
|
||||||
export class WebSocketClient extends EventEmitter {
|
export class WebSocketClient extends EventEmitter {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
|
|||||||
@@ -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[]>([]);
|
||||||
@@ -52,6 +54,10 @@ export function App() {
|
|||||||
const [alertNotification, setAlertNotification] = useState<AlertNotification | null>(null);
|
const [alertNotification, setAlertNotification] = useState<AlertNotification | null>(null);
|
||||||
const alertAudioRef = useRef<HTMLAudioElement | null>(null);
|
const alertAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
// Custom AUEC Amount State
|
||||||
|
const [customAuecAmount, setCustomAuecAmount] = useState<number | null>(null);
|
||||||
|
const [customAuecInput, setCustomAuecInput] = useState('');
|
||||||
|
|
||||||
// Load alerts from database on mount
|
// Load alerts from database on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAlerts = async () => {
|
const loadAlerts = async () => {
|
||||||
@@ -65,6 +71,22 @@ export function App() {
|
|||||||
loadAlerts();
|
loadAlerts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load custom AUEC amount from database on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCustomAmount = async () => {
|
||||||
|
try {
|
||||||
|
const amount = await window.electron.ipcRenderer.invoke('settings:getCustomAuecAmount');
|
||||||
|
if (amount) {
|
||||||
|
setCustomAuecAmount(amount);
|
||||||
|
setCustomAuecInput(amount.toString());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load custom AUEC amount:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadCustomAmount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch initial data on mount
|
// Fetch initial data on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
@@ -242,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
|
||||||
@@ -314,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -374,28 +399,40 @@ export function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveCustomAuecAmount = async () => {
|
||||||
|
const amount = parseFloat(customAuecInput);
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
alert('Please enter a valid AUEC amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electron.ipcRenderer.invoke('settings:setCustomAuecAmount', amount);
|
||||||
|
setCustomAuecAmount(amount);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save custom AUEC amount:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const dismissNotification = () => {
|
const dismissNotification = () => {
|
||||||
setAlertNotification(null);
|
setAlertNotification(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
});
|
});
|
||||||
@@ -403,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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -439,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',
|
||||||
@@ -465,7 +617,7 @@ export function App() {
|
|||||||
<div style={{ flex: 1, backgroundColor: '#1a1f3a', padding: '15px', borderRadius: '8px' }}>
|
<div style={{ flex: 1, backgroundColor: '#1a1f3a', padding: '15px', borderRadius: '8px' }}>
|
||||||
<div style={{ fontSize: '12px', color: '#888', marginBottom: '5px' }}>LOWEST PRICE</div>
|
<div style={{ fontSize: '12px', color: '#888', marginBottom: '5px' }}>LOWEST PRICE</div>
|
||||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#50e3c2' }}>
|
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#50e3c2' }}>
|
||||||
${latestPrice.lowestPrice.toFixed(4)}
|
${latestPrice.lowestPrice.toFixed(9)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: '#888' }}>per 1M AUEC</div>
|
<div style={{ fontSize: '12px', color: '#888' }}>per 1M AUEC</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -552,7 +704,7 @@ export function App() {
|
|||||||
{alert.auecAmount.toLocaleString()} AUEC for ${alert.maxPrice.toFixed(2)}
|
{alert.auecAmount.toLocaleString()} AUEC for ${alert.maxPrice.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: '#888' }}>
|
<div style={{ fontSize: '12px', color: '#888' }}>
|
||||||
${(alert.maxPrice / (alert.auecAmount / 1000000)).toFixed(4)} per 1M AUEC
|
${(alert.maxPrice / (alert.auecAmount / 1000000)).toFixed(9)} per 1M AUEC
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
@@ -681,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"
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ color: '#888', fontSize: '11px', fontStyle: 'italic' }}>
|
<div style={{ color: '#888', fontSize: '11px', fontStyle: 'italic' }}>
|
||||||
@@ -719,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(4)}`, name]}
|
|
||||||
wrapperStyle={{ zIndex: 1000 }}
|
wrapperStyle={{ zIndex: 1000 }}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
@@ -740,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' }}>
|
||||||
@@ -769,7 +1050,41 @@ export function App() {
|
|||||||
|
|
||||||
{latestPrice && (
|
{latestPrice && (
|
||||||
<div style={{ backgroundColor: '#1a1f3a', padding: '20px', borderRadius: '8px' }}>
|
<div style={{ backgroundColor: '#1a1f3a', padding: '20px', borderRadius: '8px' }}>
|
||||||
<h2 style={{ margin: '0 0 15px 0' }}>Current Listings ({latestPrice.allPrices.length})</h2>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
||||||
|
<h2 style={{ margin: 0 }}>Current Listings ({latestPrice.allPrices.length})</h2>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter AUEC amount"
|
||||||
|
value={customAuecInput}
|
||||||
|
onChange={(e) => setCustomAuecInput(e.target.value)}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: '#0a0e27',
|
||||||
|
border: '1px solid #2a2f4a',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '14px',
|
||||||
|
width: '180px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={saveCustomAuecAmount}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
backgroundColor: '#50e3c2',
|
||||||
|
color: '#0a0e27',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Set Amount
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
@@ -783,16 +1098,28 @@ export function App() {
|
|||||||
<th style={{ textAlign: 'left', padding: '12px', color: '#888', fontWeight: 'normal' }}>Platform</th>
|
<th style={{ textAlign: 'left', padding: '12px', color: '#888', fontWeight: 'normal' }}>Platform</th>
|
||||||
<th style={{ textAlign: 'left', padding: '12px', color: '#888', fontWeight: 'normal' }}>Seller</th>
|
<th style={{ textAlign: 'left', padding: '12px', color: '#888', fontWeight: 'normal' }}>Seller</th>
|
||||||
<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 && (
|
||||||
|
<th style={{ textAlign: 'right', padding: '12px', color: '#888', fontWeight: 'normal' }}>
|
||||||
|
Price for {customAuecAmount.toLocaleString()} AUEC
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{latestPrice.allPrices.map((price) => (
|
{[...latestPrice.allPrices]
|
||||||
|
.sort((a, b) => a.pricePerMillion - b.pricePerMillion)
|
||||||
|
.map((price) => (
|
||||||
<tr key={price.id} style={{ borderBottom: '1px solid #2a2f4a' }}>
|
<tr key={price.id} style={{ borderBottom: '1px solid #2a2f4a' }}>
|
||||||
<td style={{ padding: '12px' }}>{price.platform}</td>
|
<td style={{ padding: '12px' }}>{price.platform}</td>
|
||||||
<td style={{ padding: '12px' }}>{price.sellerName}</td>
|
<td style={{ padding: '12px' }}>{price.sellerName}</td>
|
||||||
<td style={{ textAlign: 'right', padding: '12px', color: '#50e3c2', fontWeight: 'bold' }}>
|
<td style={{ textAlign: 'right', padding: '12px', color: '#50e3c2', fontWeight: 'bold' }}>
|
||||||
${price.pricePerMillion.toFixed(4)}
|
${price.pricePerMillion.toFixed(9)}
|
||||||
</td>
|
</td>
|
||||||
|
{customAuecAmount && (
|
||||||
|
<td style={{ textAlign: 'right', padding: '12px', color: '#50e3c2', fontWeight: 'bold' }}>
|
||||||
|
${((price.pricePerMillion * customAuecAmount) / 1000000).toFixed(2)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -847,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "commonjs",
|
"module": "ES2020",
|
||||||
"lib": ["ES2020", "DOM"],
|
"lib": ["ES2020", "DOM"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
"types": ["node", "electron"],
|
"types": ["node", "electron"],
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/main",
|
"outDir": "./dist/main",
|
||||||
"rootDir": "./src"
|
"rootDir": "./src",
|
||||||
|
"module": "commonjs"
|
||||||
},
|
},
|
||||||
"include": ["src/preload.ts", "src/shared/**/*"]
|
"include": ["src/preload.ts", "src/shared/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import react from '@vitejs/plugin-react';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
root: '.',
|
root: '.',
|
||||||
|
base: './', // Use relative paths for Electron
|
||||||
|
publicDir: 'public',
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist/renderer',
|
outDir: 'dist/renderer',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
Reference in New Issue
Block a user