Skip to content

Commit c14a527

Browse files
authored
Merge branch 'main' into update-api-contracts-and-some-asserts-fakes
2 parents f89b3f9 + 379fb77 commit c14a527

File tree

29 files changed

+646
-425
lines changed

29 files changed

+646
-425
lines changed

.github/workflows/phpstan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
3131

3232
- name: Cache Composer dependencies
33-
uses: actions/cache@v4
33+
uses: actions/cache@v5
3434
with:
3535
path: ${{ steps.composer-cache.outputs.dir }}
3636
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}

config/nativephp.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
/**
103103
* The updater provider to use.
104104
* Supported: "github", "s3", "spaces"
105+
* Note: The "s3" provider is compatible with S3-compatible services like Cloudflare R2.
105106
*/
106107
'default' => env('NATIVEPHP_UPDATER_PROVIDER', 'spaces'),
107108

@@ -113,6 +114,7 @@
113114
'token' => env('GITHUB_TOKEN'),
114115
'vPrefixedTagName' => env('GITHUB_V_PREFIXED_TAG_NAME', true),
115116
'private' => env('GITHUB_PRIVATE', false),
117+
'autoupdate_token' => env('GITHUB_AUTOUPDATE_TOKEN'), // Read-only token used by the updater for private repos
116118
'channel' => env('GITHUB_CHANNEL', 'latest'),
117119
'releaseType' => env('GITHUB_RELEASE_TYPE', 'draft'),
118120
],
@@ -125,6 +127,13 @@
125127
'bucket' => env('AWS_BUCKET'),
126128
'endpoint' => env('AWS_ENDPOINT'),
127129
'path' => env('NATIVEPHP_UPDATER_PATH', null),
130+
/**
131+
* Optional public URL for serving updates (e.g., CDN or custom domain).
132+
* When set, updates will be downloaded from this URL instead of the S3 endpoint.
133+
* Useful for S3 with CloudFront or Cloudflare R2 with public access
134+
* Example: 'https://updates.yourdomain.com'
135+
*/
136+
'public_url' => env('AWS_PUBLIC_URL'),
128137
],
129138

130139
'spaces' => [

resources/electron/electron-plugin/dist/index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,16 @@ class NativePHP {
158158
}
159159
}
160160
startAutoUpdater(config) {
161-
var _a;
161+
var _a, _b, _c, _d, _e;
162162
if (((_a = config === null || config === void 0 ? void 0 : config.updater) === null || _a === void 0 ? void 0 : _a.enabled) === true) {
163+
const defaultProvider = (_b = config === null || config === void 0 ? void 0 : config.updater) === null || _b === void 0 ? void 0 : _b.default;
164+
const publicUrl = (_e = (_d = (_c = config === null || config === void 0 ? void 0 : config.updater) === null || _c === void 0 ? void 0 : _c.providers) === null || _d === void 0 ? void 0 : _d[defaultProvider]) === null || _e === void 0 ? void 0 : _e.public_url;
165+
if (publicUrl) {
166+
autoUpdater.setFeedURL({
167+
provider: 'generic',
168+
url: publicUrl
169+
});
170+
}
163171
autoUpdater.checkForUpdatesAndNotify();
164172
}
165173
}

resources/electron/electron-plugin/dist/server/api/menuBar.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ router.post("/context-menu", (req, res) => {
3131
const { contextMenu } = req.body;
3232
(_a = state.tray) === null || _a === void 0 ? void 0 : _a.setContextMenu(buildMenu(contextMenu));
3333
});
34+
router.post("/show-context-menu", (req, res) => {
35+
var _a;
36+
res.sendStatus(200);
37+
(_a = state.tray) === null || _a === void 0 ? void 0 : _a.popUpContextMenu();
38+
});
3439
router.post("/show", (req, res) => {
3540
res.sendStatus(200);
3641
state.activeMenuBar.showWindow();

resources/electron/electron-plugin/dist/server/api/notification.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,49 @@
11
import express from 'express';
22
import { Notification } from 'electron';
3-
import { notifyLaravel } from "../utils.js";
3+
import { notifyLaravel, broadcastToWindows } from "../utils.js";
4+
import playSoundLib from 'play-sound';
5+
import fs from 'fs';
6+
const isLocalFile = (sound) => {
7+
if (typeof sound !== 'string')
8+
return false;
9+
if (/^https?:\/\//i.test(sound))
10+
return false;
11+
return sound.includes('/') || sound.includes('\\');
12+
};
413
const router = express.Router();
514
router.post('/', (req, res) => {
615
const { title, body, subtitle, silent, icon, hasReply, timeoutType, replyPlaceholder, sound, urgency, actions, closeButtonText, toastXml, event: customEvent, reference, } = req.body;
716
const eventName = customEvent !== null && customEvent !== void 0 ? customEvent : '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked';
817
const notificationReference = reference !== null && reference !== void 0 ? reference : (Date.now() + '.' + Math.random().toString(36).slice(2, 9));
18+
const usingLocalFile = isLocalFile(sound);
919
const notification = new Notification({
1020
title,
1121
body,
1222
subtitle,
13-
silent,
23+
silent: usingLocalFile ? true : silent,
1424
icon,
1525
hasReply,
1626
timeoutType,
1727
replyPlaceholder,
18-
sound,
28+
sound: usingLocalFile ? undefined : sound,
1929
urgency,
2030
actions,
2131
closeButtonText,
2232
toastXml
2333
});
34+
if (usingLocalFile && !silent) {
35+
fs.access(sound, fs.constants.F_OK, (err) => {
36+
if (err) {
37+
broadcastToWindows('log', {
38+
level: 'error',
39+
message: `Sound file not found: ${sound}`,
40+
context: { sound }
41+
});
42+
return;
43+
}
44+
playSoundLib().play(sound, () => { });
45+
});
46+
}
2447
notification.on("click", (event) => {
2548
notifyLaravel('events', {
2649
event: eventName || '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked',

resources/electron/electron-plugin/dist/server/api/system.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ router.post('/print-to-pdf', (req, res) => __awaiter(void 0, void 0, void 0, fun
107107
});
108108
});
109109
});
110-
yield printWindow.loadURL(`data:text/html;charset=UTF-8,${html}`);
110+
yield printWindow.loadURL(`data:text/html;base64;charset=UTF-8,${html}`);
111111
}));
112112
router.get('/theme', (req, res) => {
113113
res.json({

resources/electron/electron-plugin/dist/server/api/window.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ router.post('/closable', (req, res) => {
4141
(_a = state.windows[id]) === null || _a === void 0 ? void 0 : _a.setClosable(closable);
4242
res.sendStatus(200);
4343
});
44+
router.post('/window-button-visibility', (req, res) => {
45+
var _a;
46+
const { id, windowButtonVisibility } = req.body;
47+
(_a = state.windows[id]) === null || _a === void 0 ? void 0 : _a.setWindowButtonVisibility(windowButtonVisibility);
48+
res.sendStatus(200);
49+
});
4450
router.post('/show-dev-tools', (req, res) => {
4551
var _a;
4652
const { id } = req.body;
@@ -145,7 +151,7 @@ function getWindowData(id) {
145151
};
146152
}
147153
router.post('/open', (req, res) => {
148-
let { id, x, y, frame, width, height, minWidth, minHeight, maxWidth, maxHeight, focusable, skipTaskbar, hiddenInMissionControl, hasShadow, url, resizable, movable, minimizable, maximizable, closable, title, alwaysOnTop, titleBarStyle, trafficLightPosition, vibrancy, backgroundColor, transparency, showDevTools, fullscreen, fullscreenable, kiosk, autoHideMenuBar, webPreferences, zoomFactor, preventLeaveDomain, preventLeavePage, suppressNewWindows, } = req.body;
154+
let { id, x, y, frame, width, height, minWidth, minHeight, maxWidth, maxHeight, focusable, skipTaskbar, hiddenInMissionControl, hasShadow, url, resizable, movable, minimizable, maximizable, closable, title, alwaysOnTop, titleBarStyle, trafficLightPosition, windowButtonVisibility, vibrancy, backgroundColor, transparency, showDevTools, fullscreen, fullscreenable, kiosk, autoHideMenuBar, webPreferences, zoomFactor, preventLeaveDomain, preventLeavePage, suppressNewWindows, } = req.body;
149155
if (state.windows[id]) {
150156
state.windows[id].show();
151157
state.windows[id].focus();
@@ -193,6 +199,9 @@ router.post('/open', (req, res) => {
193199
return { action: "deny" };
194200
});
195201
}
202+
if (process.platform === 'darwin') {
203+
window.setWindowButtonVisibility(windowButtonVisibility);
204+
}
196205
window.on('blur', () => {
197206
notifyLaravel('events', {
198207
event: 'Native\\Desktop\\Events\\Windows\\WindowBlurred',

resources/electron/electron-plugin/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ class NativePHP {
3434
cert: string,
3535
appPath: string
3636
) {
37-
3837
initialize();
3938

4039
state.icon = icon;
@@ -216,6 +215,17 @@ class NativePHP {
216215

217216
private startAutoUpdater(config) {
218217
if (config?.updater?.enabled === true) {
218+
// If a public URL is configured for the current provider, use it for updates
219+
const defaultProvider = config?.updater?.default;
220+
const publicUrl = config?.updater?.providers?.[defaultProvider]?.public_url;
221+
222+
if (publicUrl) {
223+
autoUpdater.setFeedURL({
224+
provider: 'generic',
225+
url: publicUrl
226+
});
227+
}
228+
219229
autoUpdater.checkForUpdatesAndNotify();
220230
}
221231
}

resources/electron/electron-plugin/src/server/api/menuBar.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ router.post("/context-menu", (req, res) => {
4242
state.tray?.setContextMenu(buildMenu(contextMenu));
4343
});
4444

45+
router.post("/show-context-menu", (req, res) => {
46+
res.sendStatus(200);
47+
48+
state.tray?.popUpContextMenu();
49+
});
50+
4551
router.post("/show", (req, res) => {
4652
res.sendStatus(200);
4753

resources/electron/electron-plugin/src/server/api/notification.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import express from 'express';
22
import { Notification } from 'electron';
3-
import {notifyLaravel} from "../utils.js";
3+
import {notifyLaravel, broadcastToWindows} from "../utils.js";
4+
declare const require: any;
5+
import playSoundLib from 'play-sound';
6+
import fs from 'fs';
7+
8+
const isLocalFile = (sound: unknown) => {
9+
if (typeof sound !== 'string') return false;
10+
if (/^https?:\/\//i.test(sound)) return false;
11+
// Treat any string containing path separators as a local file
12+
return sound.includes('/') || sound.includes('\\');
13+
};
414
const router = express.Router();
515

616
router.post('/', (req, res) => {
@@ -26,22 +36,39 @@ router.post('/', (req, res) => {
2636

2737
const notificationReference = reference ?? (Date.now() + '.' + Math.random().toString(36).slice(2, 9));
2838

39+
const usingLocalFile = isLocalFile(sound);
40+
2941
const notification = new Notification({
3042
title,
3143
body,
3244
subtitle,
33-
silent,
45+
silent: usingLocalFile ? true : silent,
3446
icon,
3547
hasReply,
3648
timeoutType,
3749
replyPlaceholder,
38-
sound,
50+
sound: usingLocalFile ? undefined : sound,
3951
urgency,
4052
actions,
4153
closeButtonText,
4254
toastXml
4355
});
4456

57+
if (usingLocalFile && !silent) {
58+
fs.access(sound, fs.constants.F_OK, (err) => {
59+
if (err) {
60+
broadcastToWindows('log', {
61+
level: 'error',
62+
message: `Sound file not found: ${sound}`,
63+
context: { sound }
64+
});
65+
return;
66+
}
67+
68+
playSoundLib().play(sound, () => {});
69+
});
70+
}
71+
4572
notification.on("click", (event) => {
4673
notifyLaravel('events', {
4774
event: eventName || '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked',

0 commit comments

Comments
 (0)