|
1 | | -const { app, BrowserWindow, ipcMain } = require('electron'); |
2 | | -const { exec } = require('child_process'); |
3 | | -const path = require('path'); |
4 | | - |
5 | | -function createWindow() { |
6 | | - const win = new BrowserWindow({ |
7 | | - width: 1280, |
8 | | - height: 900, |
9 | | - webPreferences: { |
10 | | - nodeIntegration: true, |
11 | | - contextIsolation: false |
12 | | - }, |
13 | | - icon: path.join(__dirname, 'images/hackeros.png') |
14 | | - }); |
15 | | - |
16 | | - win.loadFile('index.html'); |
17 | | - win.setMenuBarVisibility(false); |
| 1 | +import sys |
| 2 | +import os |
| 3 | +import subprocess |
| 4 | +from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGridLayout, QMainWindow |
| 5 | +from PySide6.QtGui import QFont, QPixmap, QIcon, QFontDatabase, QLinearGradient, QBrush, QColor, QPainter, QPalette, QMovie |
| 6 | +from PySide6.QtCore import Qt, QPropertyAnimation, QSequentialAnimationGroup, QRect, QTimer, QAbstractAnimation, QEasingCurve, QUrl, QPoint, QElapsedTimer |
| 7 | + |
| 8 | +# Use system icons directory |
| 9 | +IMAGES_DIR = '/usr/share/HackerOS/ICONS' |
| 10 | + |
| 11 | +# Game paths and commands (hardcoded as in original) |
| 12 | +GAME_PATHS = { |
| 13 | + 'starblaster': '/usr/share/HackerOS/Scripts/HackerOS-Games/starblaster', |
| 14 | + 'bit-jump': '/usr/share/HackerOS/Scripts/HackerOS-Games/bit-jump.love', |
| 15 | + 'the-racer': '/usr/share/HackerOS/Scripts/HackerOS-Games/the-racer' |
18 | 16 | } |
| 17 | +LAUNCH_COMMANDS = { |
| 18 | + 'starblaster': [GAME_PATHS['starblaster']], |
| 19 | + 'bit-jump': ['love', GAME_PATHS['bit-jump']], |
| 20 | + 'the-racer': [GAME_PATHS['the-racer']] |
| 21 | +} |
| 22 | + |
| 23 | +class ParticleWidget(QWidget): |
| 24 | + def __init__(self, parent=None): |
| 25 | + super().__init__(parent) |
| 26 | + self.setAttribute(Qt.WA_TransparentForMouseEvents) |
| 27 | + self.particles = [ |
| 28 | + {'cx_start': 0.10, 'cx_end': 0.15, 'cx_dur': 4, 'cy_start': 0.15, 'cy_end': 0.85, 'cy_dur': 4, 'r': 5, 'color': QColor(57, 255, 20, 178)}, |
| 29 | + {'cx_start': 0.30, 'cx_end': 0.35, 'cx_dur': 5, 'cy_start': 0.50, 'cy_end': 0.95, 'cy_dur': 5, 'r': 4, 'color': QColor(0, 183, 235, 178)}, |
| 30 | + {'cx_start': 0.60, 'cx_end': 0.55, 'cx_dur': 4.5, 'cy_start': 0.25, 'cy_end': 0.75, 'cy_dur': 4.5, 'r': 6, 'color': QColor(255, 7, 58, 178)}, |
| 31 | + {'cx_start': 0.80, 'cx_end': 0.85, 'cx_dur': 5.5, 'cy_start': 0.40, 'cy_end': 0.90, 'cy_dur': 5.5, 'r': 3, 'color': QColor(255, 255, 255, 153)}, |
| 32 | + {'cx_start': 0.20, 'cx_end': 0.25, 'cx_dur': 4.8, 'cy_start': 0.70, 'cy_end': 0.20, 'cy_dur': 4.8, 'r': 4, 'color': QColor(57, 255, 20, 178)} |
| 33 | + ] |
| 34 | + self.elapsed = QElapsedTimer() |
| 35 | + self.elapsed.start() |
| 36 | + self.timer = QTimer(self) |
| 37 | + self.timer.timeout.connect(self.update) |
| 38 | + self.timer.start(16) # ~60 FPS |
| 39 | + |
| 40 | + def paintEvent(self, event): |
| 41 | + painter = QPainter(self) |
| 42 | + painter.setRenderHint(QPainter.Antialiasing) |
| 43 | + w, h = self.width(), self.height() |
| 44 | + for p in self.particles: |
| 45 | + # cx |
| 46 | + dur_cx = p['cx_dur'] * 1000 |
| 47 | + t_cx = self.elapsed.elapsed() % dur_cx |
| 48 | + frac_cx = t_cx / dur_cx |
| 49 | + cx = p['cx_start'] + frac_cx * (p['cx_end'] - p['cx_start']) |
| 50 | + cx_abs = cx * w |
| 51 | + # cy |
| 52 | + dur_cy = p['cy_dur'] * 1000 |
| 53 | + t_cy = self.elapsed.elapsed() % dur_cy |
| 54 | + frac_cy = t_cy / dur_cy |
| 55 | + cy = p['cy_start'] + frac_cy * (p['cy_end'] - p['cy_start']) |
| 56 | + cy_abs = cy * h |
| 57 | + painter.setBrush(QBrush(p['color'])) |
| 58 | + painter.setPen(Qt.NoPen) |
| 59 | + painter.drawEllipse(int(cx_abs - p['r']), int(cy_abs - p['r']), int(p['r']*2), int(p['r']*2)) |
| 60 | + |
| 61 | +class GameCard(QWidget): |
| 62 | + def __init__(self, game_name, image_file, color_class, parent=None): |
| 63 | + super().__init__(parent) |
| 64 | + self.game_name = game_name |
| 65 | + layout = QVBoxLayout(self) |
| 66 | + layout.setContentsMargins(40, 40, 40, 40) # p-10 |
| 67 | + |
| 68 | + # Image |
| 69 | + self.image_label = QLabel() |
| 70 | + image_path = os.path.join(IMAGES_DIR, image_file) |
| 71 | + pixmap = QPixmap(image_path) |
| 72 | + self.image_label.setPixmap(pixmap.scaledToHeight(320, Qt.SmoothTransformation)) # h-80 approximate |
| 73 | + self.image_label.setAlignment(Qt.AlignCenter) |
| 74 | + layout.addWidget(self.image_label) |
| 75 | + |
| 76 | + # Animate pixel-bounce |
| 77 | + self.bounce_anim = QPropertyAnimation(self.image_label, b'pos') |
| 78 | + self.bounce_anim.setDuration(1400) |
| 79 | + self.bounce_anim.setLoopCount(-1) |
| 80 | + self.bounce_anim.setEasingCurve(QEasingCurve.InOutSine) |
| 81 | + start_pos = self.image_label.pos() |
| 82 | + self.bounce_anim.setKeyValueAt(0, start_pos) |
| 83 | + self.bounce_anim.setKeyValueAt(0.5, start_pos + QPoint(0, -25)) |
| 84 | + self.bounce_anim.setKeyValueAt(1, start_pos) |
| 85 | + self.bounce_anim.start() |
| 86 | + |
| 87 | + # Title |
| 88 | + title = QLabel(game_name.replace('-', ' ').title()) |
| 89 | + title.setFont(QFont('Press Start 2P', 24)) # text-4xl |
| 90 | + title.setAlignment(Qt.AlignCenter) |
| 91 | + title.setStyleSheet("color: white;") |
| 92 | + layout.addWidget(title) |
| 93 | + |
| 94 | + # Button |
| 95 | + button = QPushButton('Launch') |
| 96 | + button.setFont(QFont('Press Start 2P', 12)) |
| 97 | + neon_color = self.get_neon_color(color_class) |
| 98 | + dark_color = self.get_dark_color(color_class) |
| 99 | + button.setStyleSheet(f""" |
| 100 | + QPushButton {{ |
| 101 | + background-color: {neon_color}; |
| 102 | + color: black; |
| 103 | + padding: 16px; |
| 104 | + border-radius: 8px; |
| 105 | + }} |
| 106 | + QPushButton:hover {{ |
| 107 | + background-color: {dark_color}; |
| 108 | + }} |
| 109 | + """) |
| 110 | + button.clicked.connect(self.launch_game) |
| 111 | + layout.addWidget(button) |
| 112 | + |
| 113 | + # Card style |
| 114 | + shadow_color = self.get_shadow_color(color_class) |
| 115 | + self.setStyleSheet(f""" |
| 116 | + background-color: #1a2533; |
| 117 | + border-radius: 16px; |
| 118 | + border: 4px solid #ffffff55; |
| 119 | + box-shadow: 0 0 30px rgba(255, 255, 255, 0.5), {shadow_color}; |
| 120 | + """) |
| 121 | + |
| 122 | + def get_neon_color(self, color_class): |
| 123 | + if color_class == 'green': return '#39ff14' |
| 124 | + if color_class == 'blue': return '#00b7eb' |
| 125 | + if color_class == 'red': return '#ff073a' |
| 126 | + return '#39ff14' |
| 127 | + |
| 128 | + def get_dark_color(self, color_class): |
| 129 | + if color_class == 'green': return '#2ecc40' |
| 130 | + if color_class == 'blue': return '#0099c9' |
| 131 | + if color_class == 'red': return '#d90429' |
| 132 | + return '#2ecc40' |
| 133 | + |
| 134 | + def get_shadow_color(self, color_class): |
| 135 | + if color_class == 'green': return '0 0 40px rgba(57, 255, 20, 0.7), 0 0 80px rgba(57, 255, 20, 0.5)' |
| 136 | + if color_class == 'blue': return '0 0 40px rgba(0, 183, 235, 0.7), 0 0 80px rgba(0, 183, 235, 0.5)' |
| 137 | + if color_class == 'red': return '0 0 40px rgba(255, 7, 58, 0.7), 0 0 80px rgba(255, 7, 58, 0.5)' |
| 138 | + return '0 0 40px rgba(57, 255, 20, 0.7), 0 0 80px rgba(57, 255, 20, 0.5)' |
| 139 | + |
| 140 | + def launch_game(self): |
| 141 | + command = LAUNCH_COMMANDS.get(self.game_name) |
| 142 | + if command: |
| 143 | + try: |
| 144 | + subprocess.Popen(command) |
| 145 | + except Exception as e: |
| 146 | + print(f"Error launching {self.game_name}: {e}") |
| 147 | + |
| 148 | +class MainWindow(QMainWindow): |
| 149 | + def __init__(self): |
| 150 | + super().__init__() |
| 151 | + self.setWindowTitle("HackerOS Games") |
| 152 | + self.setGeometry(100, 100, 1280, 900) |
| 153 | + self.setWindowIcon(QIcon(os.path.join(IMAGES_DIR, 'HackerOS-Games.png'))) |
| 154 | + |
| 155 | + # Central widget |
| 156 | + central = QWidget() |
| 157 | + self.setCentralWidget(central) |
| 158 | + main_layout = QVBoxLayout(central) |
| 159 | + main_layout.setContentsMargins(40, 40, 40, 40) # p-10 |
| 160 | + |
| 161 | + # Background gradient |
| 162 | + palette = central.palette() |
| 163 | + gradient = QLinearGradient(0, 0, self.width(), self.height()) |
| 164 | + gradient.setColorAt(0, QColor(8, 12, 20)) |
| 165 | + gradient.setColorAt(1, QColor(22, 32, 47)) |
| 166 | + palette.setBrush(QPalette.Window, QBrush(gradient)) |
| 167 | + central.setPalette(palette) |
| 168 | + central.setAutoFillBackground(True) |
| 169 | + |
| 170 | + # Title |
| 171 | + title = QLabel("HackerOS Games") |
| 172 | + title.setFont(QFont('Press Start 2P', 48)) # text-6xl |
| 173 | + title.setAlignment(Qt.AlignCenter) |
| 174 | + title.setStyleSheet("color: #39ff14; text-shadow: 0 0 10px #39ff14, 0 0 25px #39ff14, 0 0 50px #39ff14;") |
| 175 | + main_layout.addWidget(title) |
| 176 | + |
| 177 | + # Grid for games |
| 178 | + grid = QGridLayout() |
| 179 | + grid.setSpacing(48) # gap-12 |
| 180 | + |
| 181 | + # Starblaster |
| 182 | + starblaster = GameCard('starblaster', 'starblaster.png', 'green') |
| 183 | + grid.addWidget(starblaster, 0, 0) |
| 184 | + |
| 185 | + # Bit-Jump |
| 186 | + bitjump = GameCard('bit-jump', 'Bit-Jump.png', 'blue') |
| 187 | + grid.addWidget(bitjump, 0, 1) |
| 188 | + |
| 189 | + # The-Racer |
| 190 | + racer = GameCard('the-racer', 'The-Racer.png', 'red') |
| 191 | + grid.addWidget(racer, 0, 2) |
| 192 | + |
| 193 | + main_layout.addLayout(grid) |
| 194 | + |
| 195 | + # Logo |
| 196 | + self.logo = QLabel(central) |
| 197 | + logo_pixmap = QPixmap(os.path.join(IMAGES_DIR, 'HackerOS-Games.png')).scaled(80, 80, Qt.KeepAspectRatio) |
| 198 | + self.logo.setPixmap(logo_pixmap) |
| 199 | + self.logo.setFixedSize(80, 80) |
| 200 | + self.logo.move(self.width() - 100, 24) # top-6 right-6 |
| 201 | + self.logo.raise_() |
| 202 | + |
| 203 | + # Particles |
| 204 | + self.particles = ParticleWidget(central) |
| 205 | + self.particles.setGeometry(0, 0, self.width(), self.height()) |
| 206 | + self.particles.lower() |
| 207 | + |
| 208 | + def resizeEvent(self, event): |
| 209 | + self.particles.setGeometry(0, 0, self.width(), self.height()) |
| 210 | + self.logo.move(self.width() - 100, 24) |
| 211 | + super().resizeEvent(event) |
19 | 212 |
|
20 | | -app.whenReady().then(() => { |
21 | | - createWindow(); |
22 | | - |
23 | | - app.on('activate', () => { |
24 | | - if (BrowserWindow.getAllWindows().length === 0) { |
25 | | - createWindow(); |
26 | | - } |
27 | | - }); |
28 | | -}); |
29 | | - |
30 | | -app.on('window-all-closed', () => { |
31 | | - if (process.platform !== 'darwin') { |
32 | | - app.quit(); |
33 | | - } |
34 | | -}); |
35 | | - |
36 | | -ipcMain.on('launch-game', (event, command) => { |
37 | | - exec(command, (error, stdout, stderr) => { |
38 | | - if (error) { |
39 | | - console.error(`Error launching game: ${error.message}`); |
40 | | - return; |
41 | | - } |
42 | | - if (stderr) { |
43 | | - console.error(`Stderr: ${stderr}`); |
44 | | - return; |
45 | | - } |
46 | | - console.log(`Game launched: ${stdout}`); |
47 | | - }); |
48 | | -}); |
| 213 | +if __name__ == '__main__': |
| 214 | + app = QApplication(sys.argv) |
| 215 | + # Assume 'Press Start 2P' font is installed system-wide; no need to load from file |
| 216 | + window = MainWindow() |
| 217 | + window.show() |
| 218 | + sys.exit(app.exec()) |
0 commit comments