Compare commits
No commits in common. "main" and "windows-auth-fix" have entirely different histories.
main
...
windows-au
5
.gitignore
vendored
@ -30,8 +30,3 @@ yarn-error.log*
|
|||||||
stats.html
|
stats.html
|
||||||
|
|
||||||
test-results.xml
|
test-results.xml
|
||||||
|
|
||||||
dist/*
|
|
||||||
|
|
||||||
# Native macOS vibrancy (built by scripts/build-macos-effects.mjs)
|
|
||||||
src/bun/libMacWindowEffects.dylib
|
|
||||||
154
Jenkinsfile
vendored
@ -1,57 +1,3 @@
|
|||||||
properties([
|
|
||||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
|
||||||
])
|
|
||||||
|
|
||||||
def bunBinDir() {
|
|
||||||
if (isUnix()) {
|
|
||||||
return "${env.HOME}/.bun/bin"
|
|
||||||
}
|
|
||||||
return "${env.USERPROFILE}/.bun/bin"
|
|
||||||
}
|
|
||||||
|
|
||||||
def withBun(Closure body) {
|
|
||||||
def sep = isUnix() ? ':' : ';'
|
|
||||||
withEnv(["PATH=${bunBinDir()}${sep}${env.PATH}"]) {
|
|
||||||
body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def checkBun() {
|
|
||||||
if (isUnix()) {
|
|
||||||
sh 'bun -v'
|
|
||||||
} else {
|
|
||||||
bat 'bun -v'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeBuildMetadata() {
|
|
||||||
if (isUnix()) {
|
|
||||||
sh '''
|
|
||||||
bun --eval "await Bun.write('src/buildInfo.json', JSON.stringify({ buildNumber: process.env.BUILD_NUMBER || 'dev' }, null, 2) + '\\n')"
|
|
||||||
'''
|
|
||||||
} else {
|
|
||||||
bat '''
|
|
||||||
bun --eval "await Bun.write('src/buildInfo.json', JSON.stringify({ buildNumber: process.env.BUILD_NUMBER || 'dev' }, null, 2) + '\\n')"
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def runBuild(buildCommand) {
|
|
||||||
def releaseBaseUrl =
|
|
||||||
"https://dist.farmcontrol.app/jenkins/farmcontrol/farmcontrol-ui"
|
|
||||||
withEnv([
|
|
||||||
"VITE_BUILD_NUMBER=${env.BUILD_NUMBER ?: 'dev'}",
|
|
||||||
'NODE_ENV=production',
|
|
||||||
"ELECTROBUN_RELEASE_BASE_URL=${releaseBaseUrl}",
|
|
||||||
]) {
|
|
||||||
if (isUnix()) {
|
|
||||||
sh buildCommand
|
|
||||||
} else {
|
|
||||||
bat buildCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def deploy() {
|
def deploy() {
|
||||||
node('ubuntu') {
|
node('ubuntu') {
|
||||||
try {
|
try {
|
||||||
@ -74,12 +20,13 @@ def deploy() {
|
|||||||
|
|
||||||
stage('Build (Ubuntu)') {
|
stage('Build (Ubuntu)') {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare"
|
sh 'NODE_ENV=production pnpm build:cloudflare'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Deploy (Ubuntu)') {
|
stage('Deploy (Ubuntu)') {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
|
// Deploy to Cloudflare Pages using wrangler
|
||||||
withCredentials([string(credentialsId: 'cloudflare-api-token', variable: 'CLOUDFLARE_API_TOKEN')]) {
|
withCredentials([string(credentialsId: 'cloudflare-api-token', variable: 'CLOUDFLARE_API_TOKEN')]) {
|
||||||
sh 'pnpm wrangler pages deploy build --branch main'
|
sh 'pnpm wrangler pages deploy build --branch main'
|
||||||
}
|
}
|
||||||
@ -91,101 +38,56 @@ def deploy() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def prepareMacBuildWorkspace() {
|
def buildOnLabel(label, buildCommand) {
|
||||||
if (isUnix()) {
|
|
||||||
sh '''
|
|
||||||
df -h .
|
|
||||||
for vol in /Volumes/Farm\\ Control*; do
|
|
||||||
if [ -d "$vol" ]; then
|
|
||||||
hdiutil detach "$vol" -force || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
rm -rf build/stable-macos-*/.dmg-staging build/stable-macos-*/.finalize-dmg-staging 2>/dev/null || true
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def buildOnLabel(label, buildCommand, bundleCef = false) {
|
|
||||||
def cefLabel = bundleCef ? 'cef' : 'native'
|
|
||||||
return {
|
return {
|
||||||
node(label) {
|
node(label) {
|
||||||
try {
|
stage("Checkout (${label})") {
|
||||||
stage("Checkout (${label}/${cefLabel})") {
|
|
||||||
checkout scm
|
checkout scm
|
||||||
}
|
}
|
||||||
|
|
||||||
withBun {
|
stage("Setup Node.js (${label})") {
|
||||||
stage("Check Bun (${label}/${cefLabel})") {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
checkBun()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label.startsWith('macos')) {
|
|
||||||
stage("Prepare macOS workspace (${label}/${cefLabel})") {
|
|
||||||
prepareMacBuildWorkspace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Install Dependencies (${label}/${cefLabel})") {
|
|
||||||
if (isUnix()) {
|
if (isUnix()) {
|
||||||
sh 'bun install --frozen-lockfile'
|
sh 'node -v'
|
||||||
|
sh 'pnpm -v'
|
||||||
} else {
|
} else {
|
||||||
bat 'bun install --frozen-lockfile'
|
bat 'node -v'
|
||||||
}
|
bat 'pnpm -v'
|
||||||
}
|
|
||||||
|
|
||||||
stage("Write Build Metadata (${label}/${cefLabel})") {
|
|
||||||
writeBuildMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Build (${label}/${cefLabel})") {
|
|
||||||
withEnv(["ELECTROBUN_BUNDLE_CEF=${bundleCef ? 'true' : 'false'}"]) {
|
|
||||||
runBuild(buildCommand)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage("Archive Artifacts (${label}/${cefLabel})") {
|
stage("Install Dependencies (${label})") {
|
||||||
archiveArtifacts artifacts: 'app_dist/farmcontrol-*', fingerprint: true
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
}
|
if (isUnix()) {
|
||||||
} finally {
|
sh 'pnpm install --frozen-lockfile --production=false'
|
||||||
cleanWs()
|
} else {
|
||||||
}
|
bat 'pnpm install --frozen-lockfile --production=false'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def buildMacOnLabel(label, targetArch, bundleCef = false) {
|
stage("Build (${label})") {
|
||||||
return buildOnLabel(label, "ELECTROBUN_TARGET_ARCH=${targetArch} bun run build:app:mac", bundleCef)
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
|
if (isUnix()) {
|
||||||
|
sh "NODE_ENV=production ${buildCommand}"
|
||||||
|
} else {
|
||||||
|
bat "set NODE_ENV=production && ${buildCommand}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def setBuildNameFromPackageVersion() {
|
stage("Archive Artifacts (${label})") {
|
||||||
node('ubuntu') {
|
archiveArtifacts artifacts: 'app_dist/**/*.dmg, app_dist/**/*.exe', fingerprint: true
|
||||||
stage('Set Build Name') {
|
|
||||||
checkout scm
|
|
||||||
withBun {
|
|
||||||
checkBun()
|
|
||||||
def version = sh(
|
|
||||||
script: "bun --eval \"console.log(JSON.parse(await Bun.file('package.json').text()).version)\"",
|
|
||||||
returnStdout: true
|
|
||||||
).trim()
|
|
||||||
def buildName = "v${version}-b${env.BUILD_NUMBER}"
|
|
||||||
currentBuild.displayName = buildName
|
|
||||||
echo "Build name set to: ${buildName}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setBuildNameFromPackageVersion()
|
|
||||||
|
|
||||||
parallel(
|
parallel(
|
||||||
'Windows Build': buildOnLabel('windows', 'bun run build:app', false),
|
'Windows Build': buildOnLabel('windows', 'pnpm build:electron'),
|
||||||
'Windows CEF Build': buildOnLabel('windows', 'bun run build:app', true),
|
'MacOS Build': buildOnLabel('macos', 'pnpm build:electron'),
|
||||||
'MacOS x64 Build': buildMacOnLabel('macos', 'x64', false),
|
|
||||||
'MacOS x64 CEF Build': buildMacOnLabel('macos', 'x64', true),
|
|
||||||
'MacOS arm64 Build': buildMacOnLabel('macos', 'arm64', false),
|
|
||||||
'MacOS arm64 CEF Build': buildMacOnLabel('macos', 'arm64', true),
|
|
||||||
'Ubuntu Deploy': { deploy() }
|
'Ubuntu Deploy': { deploy() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB |
BIN
assets/icon.ico
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 613 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 567 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 30 KiB |
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 72 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1.093797,0,0,1.093797,0.640357,-0.002979)">
|
|
||||||
<path d="M39.703,63.992C33.273,63.965 32.923,63.847 31.428,63.333C28.308,62.274 25.677,60.059 24.093,57.166L23.605,56.273L22.819,56.189C21.521,56.058 19.247,55.63 17.735,55.249C11.032,53.558 6.21,50.355 4.507,46.45C4.352,46.116 4.138,45.39 4.031,44.83C3.459,41.997 4.555,39.068 7.079,36.734C7.626,36.222 7.876,35.877 8.031,35.424C8.138,35.091 8.341,34.638 8.46,34.436C8.686,34.067 8.686,34.055 8.341,33.305C7.4,31.269 7.388,29.197 8.329,27.244L8.686,26.506L8.4,25.947C7.698,24.577 7.436,22.732 7.757,21.386L7.9,20.767L7.103,20.041C4.519,17.636 3.4,14.6 4.066,11.718C4.781,8.539 7.198,5.896 11.389,3.681C15.104,1.728 19.473,0.585 25.236,0.073C26.51,-0.046 31.999,0.109 33.488,0.3C40.929,1.252 46.93,3.824 50.157,7.467C53.669,11.432 53.36,16.338 49.371,20.041L48.573,20.767L48.716,21.386C49.038,22.744 48.776,24.565 48.061,25.97C47.775,26.554 47.775,26.566 48.002,26.935C48.537,27.792 48.799,28.876 48.799,30.245C48.799,30.465 48.795,30.668 48.786,30.858C47.381,30.973 46.02,31.246 44.722,31.661C45.016,31.051 45.191,29.981 45.025,29.697C44.93,29.518 44.858,29.53 44.43,29.828C41.763,31.638 38.048,32.948 33.547,33.65C32.059,33.876 31.285,33.924 28.237,33.924C25.189,33.924 24.415,33.876 22.926,33.65C18.545,32.959 14.878,31.709 12.294,29.995C11.484,29.459 11.496,29.459 11.413,29.804C11.222,30.554 11.639,31.709 12.425,32.686C13.687,34.257 16.783,35.865 20.152,36.734C25.553,38.129 31.889,38.015 36.956,36.571C35.707,37.901 34.654,39.418 33.845,41.075C32.625,41.258 31.365,41.384 30.082,41.449L28.499,41.532L29.237,42.33C29.654,42.771 30.308,43.58 30.689,44.128C31.344,45.069 31.404,45.128 31.785,45.069C31.904,45.049 32.16,45.014 32.465,44.973C32.161,46.294 32,47.67 32,49.083C32,51.584 32.504,53.969 33.415,56.141C33.042,56.019 32.813,55.848 32.404,55.606C31.63,55.141 30.725,54.118 30.344,53.26C30.213,52.963 29.951,52.058 29.749,51.26C28.82,47.45 26.891,44.664 23.688,42.509C22.272,41.556 21.39,41.163 18.414,40.187C15.759,39.306 13.496,38.317 12.294,37.496C11.484,36.948 11.508,36.948 11.413,37.424C11.175,38.71 12.294,40.437 14.223,41.747C15.342,42.497 17.259,43.366 19.176,43.997C21.188,44.652 22.2,45.176 23.224,46.069C24.927,47.545 25.546,48.76 26.189,51.808C26.76,54.522 27.57,56.082 29.213,57.642C30.356,58.725 31.416,59.38 32.797,59.845L33.797,60.19L35.768,60.196C36.883,61.649 38.21,62.93 39.703,63.992ZM10.186,43.271C9.71,42.794 9.115,42.08 8.841,41.675L8.365,40.949L8.019,41.687C7.329,43.175 7.662,44.997 8.889,46.462C10.913,48.867 15.223,50.962 20.152,51.927C22.724,52.427 22.498,52.415 22.414,52.022C22.105,50.605 21.605,49.617 20.926,49.01C20.283,48.45 19.438,48.057 17.366,47.355C16.366,47.009 15.068,46.509 14.497,46.235C13.008,45.545 11.198,44.295 10.186,43.271ZM12.818,23.601C12.187,23.327 11.603,23.065 11.532,23.041C11.425,22.994 11.413,23.089 11.472,23.387C12.079,26.066 16.437,28.649 22.081,29.673C24.224,30.066 25.534,30.173 28.237,30.173C30.94,30.173 32.249,30.066 34.392,29.673C40.036,28.649 44.394,26.066 45.001,23.387C45.061,23.089 45.049,22.994 44.942,23.041C44.87,23.065 44.287,23.327 43.656,23.601C40.751,24.875 37.274,25.744 32.987,26.268C31.261,26.482 25.212,26.482 23.486,26.268C19.2,25.744 15.723,24.875 12.818,23.601ZM31.19,3.872C23.391,3.241 15.259,5.098 10.663,8.563C9.448,9.48 8.71,10.337 8.138,11.48C7.722,12.302 7.674,12.516 7.674,13.361C7.674,14.635 8.138,15.623 9.305,16.85C13.115,20.886 22.414,23.327 31.309,22.613C38.429,22.041 44.287,19.922 47.168,16.85C48.335,15.623 48.799,14.635 48.799,13.361C48.799,12.516 48.752,12.302 48.335,11.48C47.764,10.337 47.025,9.48 45.811,8.563C42.37,5.979 37.345,4.36 31.19,3.872ZM25.796,7.932C29.999,7.265 34.869,8.527 36.595,10.718C37.357,11.694 37.548,12.218 37.548,13.361C37.548,14.516 37.357,15.04 36.595,16.016C34.488,18.683 28.249,19.791 23.677,18.302C21.355,17.54 20.057,16.576 19.223,14.992C18.795,14.183 18.795,12.552 19.223,11.742C20.259,9.765 22.533,8.444 25.796,7.932ZM30.118,11.67C27.32,11.206 23.915,11.873 22.807,13.099C22.581,13.361 22.581,13.373 22.807,13.635C23.605,14.504 26.082,15.242 28.237,15.242C30.392,15.242 32.868,14.504 33.666,13.635C33.892,13.373 33.892,13.361 33.666,13.099C33.083,12.468 31.797,11.944 30.118,11.67Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1,0,0,1,35.1075,-13.675)">
|
|
||||||
<path d="M19.178,83.675L21.86,83.675C22.896,83.675 23.714,83.056 23.949,82.006L24.677,79.011C25.075,78.891 25.513,78.706 25.906,78.51L28.52,80.101C29.389,80.672 30.396,80.644 31.191,79.849L33.035,77.979C33.748,77.216 33.821,76.234 33.251,75.277L31.691,72.637C31.913,72.276 32.021,71.92 32.166,71.528L35.186,70.826C36.217,70.564 36.892,69.747 36.892,68.705L36.892,66.06C36.892,65.038 36.191,64.272 35.186,64.011L32.197,63.289C32.038,62.825 31.867,62.484 31.722,62.148L33.37,59.446C33.904,58.571 33.804,57.481 33.086,56.8L31.18,54.956C30.427,54.209 29.411,54.031 28.562,54.596L25.906,56.269C25.436,56.028 25.075,55.914 24.683,55.769L23.949,52.722C23.714,51.712 22.896,51.042 21.86,51.042L19.178,51.042C18.131,51.042 17.304,51.737 17.063,52.728L16.381,55.737C15.886,55.903 15.519,56.028 15.101,56.269L12.444,54.596C11.633,54.068 10.585,54.204 9.852,54.956L7.997,56.8C7.279,57.501 7.077,58.571 7.642,59.446L9.316,62.148C9.166,62.484 8.981,62.876 8.81,63.289L5.852,64.011C4.841,64.272 4.177,65.064 4.177,66.06L4.177,68.705C4.177,69.747 4.847,70.564 5.852,70.826L8.847,71.528C9.037,71.92 9.182,72.276 9.342,72.637L7.782,75.302C7.211,76.234 7.311,77.267 8.023,77.979L9.847,79.849C10.585,80.638 11.613,80.646 12.487,80.101L15.158,78.51C15.596,78.757 15.989,78.891 16.381,79.011L17.063,82.006C17.299,83.005 18.131,83.675 19.178,83.675ZM20.491,72.507C17.681,72.507 15.371,70.197 15.371,67.387C15.371,64.551 17.681,62.236 20.491,62.236C23.326,62.236 25.642,64.551 25.642,67.387C25.642,70.197 23.326,72.507 20.491,72.507Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.989624,0,0,0.989624,3,3.012667)">
|
|
||||||
<path d="M32.232,55.642C32.232,57.251 30.901,58.583 29.291,58.583C27.682,58.583 26.351,57.251 26.351,55.642C26.351,54.032 27.682,52.701 29.291,52.701C30.901,52.701 32.232,54.032 32.232,55.642ZM32.232,46.867C32.232,48.476 30.901,49.807 29.291,49.807C27.682,49.807 26.351,48.476 26.351,46.867C26.351,45.258 27.682,43.926 29.291,43.926C30.901,43.926 32.232,45.258 32.232,46.867ZM32.232,38.066C32.232,39.676 30.901,41.007 29.291,41.007C28.398,41.007 27.591,40.597 27.05,39.956C27.916,38.466 28.763,36.862 29.604,35.143C31.07,35.301 32.232,36.562 32.232,38.066ZM58.608,29.291C58.608,30.901 57.277,32.232 55.667,32.232C54.058,32.232 52.727,30.901 52.727,29.291C52.727,27.682 54.058,26.351 55.667,26.351C57.277,26.351 58.608,27.682 58.608,29.291ZM5.881,29.291C5.881,30.901 4.55,32.232 2.941,32.232C1.331,32.232 0,30.901 0,29.291C0,27.682 1.331,26.351 2.941,26.351C4.55,26.351 5.881,27.682 5.881,29.291ZM14.656,29.291C14.656,30.901 13.351,32.232 11.716,32.232C10.106,32.232 8.775,30.901 8.775,29.291C8.775,27.682 10.106,26.351 11.716,26.351C13.351,26.351 14.656,27.682 14.656,29.291ZM23.457,29.291C23.457,30.901 22.126,32.232 20.516,32.232C18.907,32.232 17.576,30.901 17.576,29.291C17.576,27.682 18.907,26.351 20.516,26.351C22.126,26.351 23.457,27.682 23.457,29.291ZM41.032,29.291C41.032,30.901 39.701,32.232 38.092,32.232C36.483,32.232 35.151,30.901 35.151,29.291C35.151,27.682 36.483,26.351 38.092,26.351C39.701,26.351 41.032,27.682 41.032,29.291ZM49.807,29.291C49.807,30.901 48.476,32.232 46.867,32.232C45.258,32.232 43.926,30.901 43.926,29.291C43.926,27.682 45.258,26.351 46.867,26.351C48.476,26.351 49.807,27.682 49.807,29.291ZM31.442,31.281C31.58,30.996 31.718,30.691 31.857,30.384C31.977,30.118 32.096,29.855 32.214,29.607C32.145,30.248 31.864,30.83 31.442,31.281ZM26.771,28.065L26.377,28.924C26.451,28.32 26.715,27.77 27.108,27.338L26.771,28.065ZM31.523,18.59C30.658,20.084 29.813,21.691 28.972,23.413C27.51,23.255 26.351,22.016 26.351,20.491C26.351,18.881 27.682,17.55 29.291,17.55C30.179,17.55 30.983,17.955 31.523,18.59ZM32.232,11.716C32.232,13.325 30.901,14.656 29.291,14.656C27.682,14.656 26.351,13.325 26.351,11.716C26.351,10.106 27.682,8.775 29.291,8.775C30.901,8.775 32.232,10.106 32.232,11.716ZM32.232,2.941C32.232,4.55 30.901,5.881 29.291,5.881C27.682,5.881 26.351,4.55 26.351,2.941C26.351,1.331 27.682,0 29.291,0C30.901,0 32.232,1.331 32.232,2.941Z" style="fill-rule:nonzero;"/>
|
|
||||||
<path d="M2.803,52.396C1.307,52.643 0.237,54.108 0.504,55.597C0.751,57.093 2.216,58.169 3.711,57.928C17.696,55.598 24.556,46.57 31.857,30.384C38.113,16.508 43.608,8.067 55.805,6.052C57.295,5.806 58.371,4.341 58.129,2.851C57.851,1.356 56.392,0.28 54.897,0.521C40.932,2.825 34.078,11.879 26.771,28.065C20.515,41.941 15,50.382 2.803,52.396Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.824327,0,0,0.976141,2.488276,-2.712014)">
|
|
||||||
<path d="M56.78,22.995C64.017,23.632 69.667,28.805 69.667,35.084L69.667,42.696C69.667,49.405 63.217,54.851 55.273,54.851L52.514,54.851L39.525,64.685C37.406,66.289 34.196,66.289 32.077,64.685L19.088,54.851L16.329,54.851C8.385,54.851 1.935,49.405 1.935,42.696L1.935,35.084C1.935,28.805 7.585,23.632 14.822,22.995L14.822,8.007C14.822,6.476 16.294,5.233 18.107,5.233C19.921,5.233 21.393,6.476 21.393,8.007L21.393,22.929L50.209,22.929L50.209,8.007C50.209,6.476 51.681,5.233 53.495,5.233C55.308,5.233 56.78,6.476 56.78,8.007L56.78,22.995ZM28.932,54.851L35.801,60.052L42.67,54.851L28.932,54.851ZM55.273,49.302C59.591,49.302 63.096,46.342 63.096,42.696L63.096,35.084C63.096,31.438 59.591,28.478 55.273,28.478L16.329,28.478C12.011,28.478 8.506,31.438 8.506,35.084L8.506,42.696C8.506,46.342 12.011,49.302 16.329,49.302L55.273,49.302Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.881796,0,0,0.881796,2.413866,6)">
|
|
||||||
<path d="M8.144,48.624L11.105,53.718L33.55,42.441L55.994,53.718L58.987,48.624L36.537,37.341L36.537,10.769L30.594,10.769L30.594,37.341L8.144,48.624ZM0.384,53.413C-0.592,55.654 0.33,57.211 2.725,57.489L15.69,58.924C18.959,59.318 20.849,57.171 19.138,54.231L11.408,40.828C9.765,37.956 6.931,38.488 5.654,41.467L0.384,53.413ZM66.721,53.413L61.477,41.467C60.174,38.488 57.366,37.956 55.717,40.828L47.962,54.231C46.256,57.171 48.172,59.318 51.415,58.924L64.38,57.489C66.77,57.211 67.697,55.654 66.721,53.413ZM31.172,1.489L23.477,11.964C21.527,14.591 22.46,17.332 25.814,17.332L41.248,17.332C44.665,17.332 45.558,14.639 43.592,11.97L35.87,1.484C34.395,-0.486 32.642,-0.506 31.172,1.489Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -2,9 +2,6 @@
|
|||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
<g transform="matrix(0.069972,0,0,0.069972,-2.986098,-11.802594)">
|
<g transform="matrix(0.069972,0,0,0.069972,-2.986098,-11.802594)">
|
||||||
<path d="M242,226C206,232 174.833,248.333 148.5,275C122.167,301.667 106,333 100,369L100,884C106,919.333 122,950 148,976C174,1002 204.333,1018.333 239,1025L243,1026L758,1026L761,1025C796.333,1018.333 826.833,1001.833 852.5,975.5C878.167,949.167 894,918.333 900,883L900,368C894,332.667 877.667,301.667 851,275C824.333,248.333 793,232 757,226L242,226Z" style="fill:rgb(255,152,0);fill-rule:nonzero;"/>
|
<path d="M242,226C206,232 174.833,248.333 148.5,275C122.167,301.667 106,333 100,369L100,884C106,919.333 122,950 148,976C174,1002 204.333,1018.333 239,1025L243,1026L758,1026L761,1025C796.333,1018.333 826.833,1001.833 852.5,975.5C878.167,949.167 894,918.333 900,883L900,368C894,332.667 877.667,301.667 851,275C824.333,248.333 793,232 757,226L242,226ZM802,325L802,401C731.333,401.667 636.667,401.667 518,401L519,326L802,325ZM481,326C481.667,342.667 482,367.667 482,401L200,401L200,326L481,326ZM518,438L802,438L802,514L518,514L518,438ZM482,438L482,514L200,513L200,439L482,438ZM554,550C590.667,550 645.667,550.333 719,551L801,551L801,627L518,628L518,550L554,550ZM482,550L482,628L200,628L200,551L482,551L482,550ZM518,665L801,665C801.667,677 801.667,695.333 801,720L801,740L519,740L518,665ZM311,700C327.667,700 343.833,703.5 359.5,710.5C375.167,717.5 388.333,727.333 399,740C413.667,756.667 422,776.5 424,799.5C426,822.5 422,844.5 412,865.5C402,886.5 387,902 367,912C348.333,922.667 327.5,927.167 304.5,925.5C281.5,923.833 260.833,916.333 242.5,903C224.167,889.667 212,872.333 206,851C198,831.667 196.333,811.333 201,790C205.667,768.667 215.333,750.167 230,734.5C244.667,718.833 262.333,708.667 283,704C292.333,701.333 301.667,700 311,700Z" style="fill:rgb(255,152,0);fill-rule:nonzero;"/>
|
||||||
<g transform="matrix(14.291431,0,0,14.291431,42.675613,168.675956)">
|
|
||||||
<path d="M35.778,26.682C38.344,26.682 42.193,26.705 47.324,26.752L53.061,26.752L53.061,32.07L33.259,32.14L33.259,26.682L35.778,26.682ZM53.131,10.938L53.131,16.256C48.187,16.303 41.563,16.303 33.259,16.256L33.329,11.008L53.131,10.938ZM30.74,18.845L30.74,24.163L11.008,24.093L11.008,18.915L30.74,18.845ZM30.67,11.008C30.717,12.175 30.74,13.924 30.74,16.256L11.008,16.256L11.008,11.008L30.67,11.008ZM18.775,37.178C19.941,37.178 21.073,37.423 22.169,37.913C23.265,38.402 24.186,39.09 24.933,39.977C25.959,41.143 26.542,42.531 26.682,44.14C26.822,45.749 26.542,47.289 25.842,48.758C25.143,50.228 24.093,51.312 22.694,52.012C21.387,52.758 19.93,53.073 18.32,52.956C16.711,52.84 15.265,52.315 13.982,51.382C12.699,50.449 11.848,49.236 11.428,47.744C10.868,46.391 10.752,44.968 11.078,43.475C11.405,41.983 12.081,40.688 13.107,39.592C14.134,38.496 15.37,37.784 16.816,37.458C17.469,37.271 18.122,37.178 18.775,37.178ZM33.259,18.845L53.131,18.845L53.131,24.163L33.259,24.163L33.259,18.845ZM30.74,26.752L30.74,32.14L11.008,32.14L11.008,26.752L30.74,26.752ZM33.259,34.729L53.061,34.729C53.108,35.568 53.108,36.851 53.061,38.577L53.061,39.977L33.329,39.977L33.259,34.729Z" style="fill:white;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1,0,0,1,-82,0)">
|
|
||||||
<g id="Artboard1" transform="matrix(1,0,0,1,82,0)">
|
|
||||||
<rect x="0" y="0" width="64" height="64" style="fill:none;"/>
|
|
||||||
<clipPath id="_clip1">
|
|
||||||
<rect x="0" y="0" width="64" height="64"/>
|
|
||||||
</clipPath>
|
|
||||||
<g clip-path="url(#_clip1)">
|
|
||||||
<g transform="matrix(1,0,0,1,-82,0)">
|
|
||||||
<path d="M114.235,58.85L101,58.85C93.825,58.85 88,53.025 88,45.85L88,18.367C88,11.192 93.825,5.367 101,5.367L127,5.367C134.175,5.367 140,11.192 140,18.367L140,31.684C138.344,30.666 136.522,29.892 134.583,29.414L134.583,18.367C134.583,14.182 131.185,10.784 127,10.784L101,10.784C96.815,10.784 93.417,14.182 93.417,18.367L93.417,45.85C93.417,50.036 96.815,53.434 101,53.434L111.638,53.434C112.218,55.387 113.102,57.211 114.235,58.85Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.121769,0,0,0.121769,14.308464,17.834503)">
|
|
||||||
<path d="M8.945,187.103L8.945,45C8.945,41.153 10.395,37.687 13.54,34.715C16.57,31.856 19.983,30.533 23.684,30.533L101.541,30.533C105.244,30.533 108.657,31.856 111.685,34.716C114.831,37.688 116.28,41.153 116.28,45L116.28,63.239C116.28,67.084 114.831,70.551 111.685,73.523C108.657,76.383 105.244,77.706 101.541,77.706L59.112,77.706L59.112,99.952L95.825,99.952C99.527,99.952 102.939,101.273 105.967,104.134C109.115,107.105 110.563,110.572 110.563,114.418L110.563,132.658C110.563,136.503 109.116,139.968 105.968,142.941C102.939,145.803 99.527,147.124 95.825,147.124L59.112,147.123L59.112,187.103C59.112,190.807 57.79,194.219 54.928,197.248C51.956,200.394 48.491,201.842 44.646,201.842L23.412,201.842C19.565,201.842 16.1,200.393 13.128,197.247C10.268,194.219 8.946,190.807 8.945,187.103Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.121769,0,0,0.121769,-40.771914,17.834503)">
|
|
||||||
<path d="M591.369,171.626C580.987,156.298 575.724,137.883 575.724,116.331C575.724,91.129 582.921,70.216 597.085,53.512C611.534,36.472 631.157,27.818 656.07,27.818C677.329,27.818 694.356,32.953 707.26,42.842C720.259,52.803 729.209,67.651 733.791,87.574C734,88.48 734.021,89.418 733.855,90.333C733.839,90.422 733.822,90.511 733.804,90.6C732.171,90.549 730.531,90.524 728.886,90.524C713.323,90.524 698.283,92.793 684.08,97.019C683.789,96.57 683.51,96.096 683.244,95.596C683,95.139 682.809,94.657 682.674,94.157C680.956,87.783 678.047,82.918 673.679,79.732C669.187,76.455 663.286,74.99 656.07,74.99C646.542,74.99 639.4,78.26 634.849,85.227C629.672,93.155 627.252,103.55 627.252,116.331C627.252,120.193 627.479,123.842 627.944,127.277C613.205,139.652 600.767,154.681 591.369,171.626Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.485113,0,0,0.485113,32,32.108844)">
|
|
||||||
<path d="M66.016,33C66.016,51.203 51.234,66 33,66C14.797,66 0,51.203 0,33C0,14.781 14.797,0 33,0C51.234,0 66.016,14.781 66.016,33ZM27.094,19.5C24.859,19.5 23.438,20.75 23.438,22.734C23.438,24.734 24.859,25.969 27.078,25.969L31.812,25.969L35.847,25.339L31.234,29.375L20.781,39.828C20.031,40.562 19.609,41.578 19.609,42.547C19.609,44.75 21.234,46.219 23.281,46.219C24.375,46.219 25.312,45.844 26.156,45.016L36.516,34.656L40.507,30.084L39.969,34.312L39.969,38.75C39.969,40.953 41.219,42.391 43.234,42.391C45.188,42.391 46.438,40.922 46.438,38.75L46.438,23.797C46.438,20.984 44.812,19.5 42.094,19.5L27.094,19.5Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<rect id="Artboard1" x="0" y="0" width="64" height="64" style="fill:none;"/>
|
|
||||||
<g id="Artboard11" serif:id="Artboard1">
|
|
||||||
<g transform="matrix(0.787693,0,0,0.787693,6,6.006144)">
|
|
||||||
<path d="M32.595,65.998C14.563,65.781 0,51.084 0,33C0,14.781 14.781,0 33,0C51.1,0 65.797,14.564 66.013,32.597C64.003,31.361 61.8,30.409 59.459,29.798C58.946,25.474 57.39,21.467 55.038,18.044C53.252,19.386 50.843,20.54 47.998,21.441C48.59,23.904 48.996,26.56 49.179,29.359C44.485,30.163 40.247,32.317 36.882,35.406L35.437,35.406L35.437,36.848C32.698,39.819 30.69,43.476 29.7,47.53C22.786,47.922 16.949,49.55 13.541,52.117C13.713,51.988 13.892,51.861 14.082,51.738C17.087,54.794 20.831,57.118 25.018,58.434C22.762,56.224 20.842,53.084 19.416,49.278C20.904,48.804 22.542,48.417 24.297,48.124C25.593,51.493 27.274,54.199 29.137,55.808C29.348,57.858 29.815,59.833 30.505,61.701C28.928,61.293 27.428,60.491 26.037,59.349C27.43,60.505 28.934,61.317 30.515,61.729C31.07,63.227 31.769,64.655 32.595,65.998ZM6.474,35.406C6.886,40.076 8.511,44.4 11.044,48.053C12.829,46.735 15.216,45.6 18.025,44.712C17.325,41.847 16.882,38.72 16.753,35.406L6.474,35.406ZM52.484,14.02C52.335,14.133 52.181,14.244 52.014,14.35C48.978,11.231 45.176,8.866 40.916,7.541C43.22,9.772 45.175,12.973 46.615,16.866C45.134,17.339 43.502,17.726 41.755,18.022C40.131,13.743 37.888,10.527 35.437,9.228L35.437,18.669C42.758,18.365 48.953,16.691 52.484,14.02ZM30.697,4.271C28.855,4.713 27.116,5.696 25.532,7.13C27.115,5.709 28.851,4.737 30.688,4.299L30.697,4.271ZM21.697,30.562L30.594,30.562L30.594,23.513C27.88,23.402 25.274,23.103 22.853,22.645C22.247,25.113 21.847,27.793 21.697,30.562ZM40.497,7.15C38.915,5.712 37.177,4.726 35.335,4.279L35.343,4.307C37.181,4.749 38.916,5.726 40.497,7.15ZM10.972,18.051C8.486,21.673 6.889,25.948 6.478,30.562L16.757,30.562C16.893,27.318 17.327,24.253 18.005,21.438C15.168,20.539 12.761,19.388 10.972,18.051ZM25.091,7.542C20.832,8.867 17.03,11.234 13.995,14.355C13.823,14.245 13.663,14.131 13.509,14.015C17.058,16.692 23.268,18.369 30.594,18.67L30.594,9.212C28.138,10.505 25.887,13.729 24.26,18.022C22.509,17.726 20.874,17.338 19.389,16.863C20.831,12.972 22.788,9.772 25.091,7.542ZM22.875,43.507C25.29,43.051 27.889,42.754 30.594,42.643L30.594,35.406L21.687,35.406C21.835,38.238 22.247,40.982 22.875,43.507ZM43.162,22.644C40.747,23.101 38.146,23.399 35.437,23.512L35.437,30.562L44.319,30.562C44.169,27.793 43.769,25.113 43.162,22.644Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.485113,0,0,0.485113,31.974958,31.982526)">
|
|
||||||
<path d="M66.016,33C66.016,51.203 51.234,66 33,66C14.797,66 0,51.203 0,33C0,14.781 14.797,0 33,0C51.234,0 66.016,14.781 66.016,33ZM27.094,19.5C24.859,19.5 23.438,20.75 23.438,22.734C23.438,24.734 24.859,25.969 27.078,25.969L31.812,25.969L35.847,25.339L31.234,29.375L20.781,39.828C20.031,40.562 19.609,41.578 19.609,42.547C19.609,44.75 21.234,46.219 23.281,46.219C24.375,46.219 25.312,45.844 26.156,45.016L36.516,34.656L40.507,30.084L39.969,34.312L39.969,38.75C39.969,40.953 41.219,42.391 43.234,42.391C45.188,42.391 46.438,40.922 46.438,38.75L46.438,23.797C46.438,20.984 44.812,19.5 42.094,19.5L27.094,19.5Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.783639,0,0,0.783639,3,9.136381)">
|
|
||||||
<path d="M10.966,55.715L61.631,55.715C67.901,55.715 71.026,52.48 70.761,46.429L52.249,29.059C50.874,27.784 49.224,27.139 47.553,27.139C45.84,27.139 44.303,27.727 42.877,29.003L28.763,41.616L22.999,36.414C21.686,35.233 20.243,34.639 18.78,34.639C17.334,34.639 16.041,35.201 14.754,36.383L2.961,46.953C3.114,52.711 5.792,55.715 10.966,55.715ZM10.251,58.341L63.763,58.341C70.533,58.341 74.014,54.886 74.014,48.218L74.014,10.16C74.014,3.486 70.533,0.011 63.763,0.011L10.251,0.011C3.506,0.011 0,3.486 0,10.16L0,48.218C0,54.886 3.506,58.341 10.251,58.341ZM10.606,52.219C7.701,52.219 6.122,50.714 6.122,47.678L6.122,10.694C6.122,7.658 7.701,6.134 10.606,6.134L63.407,6.134C66.287,6.134 67.891,7.658 67.891,10.694L67.891,47.678C67.891,50.714 66.287,52.219 63.407,52.219L10.606,52.219Z" style="fill-rule:nonzero;"/>
|
|
||||||
<path d="M24.209,29.743C28.302,29.743 31.677,26.369 31.677,22.239C31.677,18.146 28.302,14.74 24.209,14.74C20.079,14.74 16.705,18.146 16.705,22.239C16.705,26.369 20.079,29.743 24.209,29.743Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 72 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<path d="M38.645,64.222L17.549,64.222C11.671,64.198 8.506,61.129 8.506,55.228L8.506,13.858C8.506,7.958 11.939,4.864 17.619,4.864L54.008,4.864C59.907,4.864 63.096,7.958 63.096,13.858L63.096,35.121C61.386,34.433 59.559,33.974 57.653,33.783L57.653,22.31L54.347,22.31L54.347,23.05C54.347,27.911 52.199,30.851 48.168,31.749L45.342,34.575C44.524,35.393 43.197,35.393 42.379,34.575L39.549,31.745C35.516,30.843 33.374,27.904 33.374,23.05L33.374,22.31L13.973,22.31L13.973,55.44C13.973,57.721 15.098,58.965 17.297,59.072C17.269,58.754 17.255,58.42 17.255,58.07L17.255,54.215C17.255,49.957 19.434,48.069 23.402,48.069L36.442,48.069C35.975,49.668 35.702,51.349 35.651,53.085L24.624,53.085C23.237,53.085 22.516,53.749 22.516,55.165L22.516,57.102C22.516,58.518 23.237,59.21 24.624,59.21L36.417,59.21C36.93,60.995 37.685,62.679 38.645,64.222ZM57.653,17.418L57.653,13.652C57.653,11.264 56.396,10.001 54.013,10.001L17.613,10.001C15.206,10.001 13.973,11.264 13.973,13.652L13.973,17.418L33.807,17.418C34.894,13.675 37.985,11.731 42.487,11.731L45.258,11.731C49.934,11.731 52.908,13.675 53.94,17.418L57.653,17.418ZM42.481,16.868C40.074,16.868 38.841,18.13 38.841,20.518L38.841,23.262C38.841,25.65 40.074,26.902 42.481,26.902L45.264,26.902C47.647,26.902 48.904,25.65 48.904,23.262L48.904,20.518C48.904,18.149 47.666,16.888 45.32,16.868L42.481,16.868Z"/>
|
|
||||||
<g transform="matrix(1,0,0,1,35.1075,-13.675)">
|
|
||||||
<path d="M19.178,83.675L21.86,83.675C22.896,83.675 23.714,83.056 23.949,82.006L24.677,79.011C25.075,78.891 25.513,78.706 25.906,78.51L28.52,80.101C29.389,80.672 30.396,80.644 31.191,79.849L33.035,77.979C33.748,77.216 33.821,76.234 33.251,75.277L31.691,72.637C31.913,72.276 32.021,71.92 32.166,71.528L35.186,70.826C36.217,70.564 36.892,69.747 36.892,68.705L36.892,66.06C36.892,65.038 36.191,64.272 35.186,64.011L32.197,63.289C32.038,62.825 31.867,62.484 31.722,62.148L33.37,59.446C33.904,58.571 33.804,57.481 33.086,56.8L31.18,54.956C30.427,54.209 29.411,54.031 28.562,54.596L25.906,56.269C25.436,56.028 25.075,55.914 24.683,55.769L23.949,52.722C23.714,51.712 22.896,51.042 21.86,51.042L19.178,51.042C18.131,51.042 17.304,51.737 17.063,52.728L16.381,55.737C15.886,55.903 15.519,56.028 15.101,56.269L12.444,54.596C11.633,54.068 10.585,54.204 9.852,54.956L7.997,56.8C7.279,57.501 7.077,58.571 7.642,59.446L9.316,62.148C9.166,62.484 8.981,62.876 8.81,63.289L5.852,64.011C4.841,64.272 4.177,65.064 4.177,66.06L4.177,68.705C4.177,69.747 4.847,70.564 5.852,70.826L8.847,71.528C9.037,71.92 9.182,72.276 9.342,72.637L7.782,75.302C7.211,76.234 7.311,77.267 8.023,77.979L9.847,79.849C10.585,80.638 11.613,80.646 12.487,80.101L15.158,78.51C15.596,78.757 15.989,78.891 16.381,79.011L17.063,82.006C17.299,83.005 18.131,83.675 19.178,83.675ZM20.491,72.507C17.681,72.507 15.371,70.197 15.371,67.387C15.371,64.551 17.681,62.236 20.491,62.236C23.326,62.236 25.642,64.551 25.642,67.387C25.642,70.197 23.326,72.507 20.491,72.507Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.069972,0,0,0.069972,-2.986098,-11.802594)">
|
|
||||||
<path d="M242,226C206,232 174.833,248.333 148.5,275C122.167,301.667 106,333 100,369L100,884C106,919.333 122,950 148,976C174,1002 204.333,1018.333 239,1025L243,1026L758,1026L761,1025C796.333,1018.333 826.833,1001.833 852.5,975.5C878.167,949.167 894,918.333 900,883L900,368C894,332.667 877.667,301.667 851,275C824.333,248.333 793,232 757,226L242,226Z" style="fill:rgb(204,93,21);fill-rule:nonzero;"/>
|
|
||||||
<g transform="matrix(1,0,0,1,0.001401,0)">
|
|
||||||
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
|
|
||||||
<circle cx="68" cy="189" r="24" style="fill:white;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
|
|
||||||
<path d="M160,213L126,213C126,168.016 88.984,131 44,131L44,97C107.636,97 160,149.364 160,213Z" style="fill:white;fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
|
|
||||||
<path d="M184,213C184,136.198 120.802,73 44,73L44,38C140.002,38 219,116.998 219,213L184,213Z" style="fill:white;fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.781552,0,0,0.781552,1,17.941979)">
|
|
||||||
<path d="M79.329,9.044C79.329,3.244 76.045,0.021 70.183,0.021L9.093,0.021C3.221,0.021 0,3.244 0,9.044L0,26.911C0,32.701 3.263,35.953 9.125,35.953L70.225,35.953C76.077,35.953 79.329,32.701 79.329,26.911L79.329,9.044ZM72.826,10.089L72.826,25.876C72.826,28.201 71.564,29.461 69.323,29.461L9.964,29.461C7.775,29.461 6.482,28.201 6.482,25.876L6.482,10.089C6.482,7.763 7.785,6.503 9.995,6.503L69.354,6.503C71.564,6.503 72.826,7.763 72.826,10.089ZM13.04,4.664L10.112,4.664L10.112,20.338C10.112,21.156 10.774,21.776 11.602,21.776C12.461,21.776 13.04,21.134 13.04,20.297L13.04,4.664ZM18.661,4.664L15.732,4.664L15.732,15.295C15.732,16.144 16.404,16.744 17.211,16.744C18.092,16.744 18.661,16.123 18.661,15.264L18.661,4.664ZM24.27,4.664L21.32,4.664L21.32,15.295C21.32,16.144 22.014,16.744 22.81,16.744C23.67,16.744 24.27,16.123 24.27,15.264L24.27,4.664ZM29.849,4.664L26.92,4.664L26.92,15.295C26.92,16.144 27.592,16.744 28.42,16.744C29.279,16.744 29.849,16.123 29.849,15.264L29.849,4.664ZM35.479,4.664L32.55,4.664L32.55,15.295C32.55,16.144 33.222,16.744 34.019,16.744C34.9,16.744 35.479,16.123 35.479,15.264L35.479,4.664ZM41.11,4.664L38.181,4.664L38.181,20.338C38.181,21.156 38.843,21.776 39.65,21.776C40.53,21.776 41.11,21.134 41.11,20.297L41.11,4.664ZM46.709,4.664L43.781,4.664L43.781,15.295C43.781,16.144 44.452,16.744 45.259,16.744C46.118,16.744 46.709,16.123 46.709,15.264L46.709,4.664ZM52.319,4.664L49.39,4.664L49.39,15.295C49.39,16.144 50.062,16.744 50.88,16.744C51.739,16.744 52.319,16.123 52.319,15.264L52.319,4.664ZM57.918,4.664L54.999,4.664L54.999,15.295C54.999,16.144 55.661,16.744 56.468,16.744C57.348,16.744 57.918,16.123 57.918,15.264L57.918,4.664ZM63.548,4.664L60.62,4.664L60.62,15.295C60.62,16.144 61.292,16.744 62.088,16.744C62.948,16.744 63.548,16.123 63.548,15.264L63.548,4.664ZM69.158,4.664L66.229,4.664L66.229,20.338C66.229,21.156 66.891,21.776 67.719,21.776C68.578,21.776 69.158,21.134 69.158,20.297L69.158,4.664Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.800063,0,0,0.800063,1,5.597937)">
|
|
||||||
<path d="M75.036,26.688L59.489,26.688C57.02,26.688 56.427,28.797 57.739,30.578L65.442,40.938C66.427,42.266 68.099,42.281 69.083,40.938L76.802,30.594C78.145,28.797 77.52,26.688 75.036,26.688ZM38.755,8.219C52.458,8.219 63.536,19.297 63.536,33.031C63.536,35.281 65.38,37.125 67.661,37.125C69.911,37.125 71.739,35.297 71.755,33.047C71.739,14.75 56.974,0 38.755,0C30.599,0 23.161,3.078 17.583,8.094C15.364,9.812 15.552,12.531 16.974,14.109C18.239,15.547 20.395,16.141 22.802,14.391C26.974,10.547 32.583,8.219 38.755,8.219ZM2.458,39.312L18.02,39.312C20.474,39.312 21.067,37.188 19.77,35.422L12.052,25.047C11.067,23.734 9.411,23.719 8.427,25.047L0.692,35.406C-0.651,37.188 -0.026,39.312 2.458,39.312ZM38.755,57.781C25.052,57.781 13.958,46.703 13.958,32.969C13.958,30.703 12.114,28.859 9.833,28.859C7.583,28.859 5.755,30.688 5.739,32.938C5.755,51.234 20.52,66 38.755,66C46.911,66 54.349,62.922 59.911,57.906C62.13,56.156 61.942,53.469 60.52,51.875C59.255,50.453 57.099,49.844 54.692,51.594C50.52,55.422 44.911,57.781 38.755,57.781Z" style="fill-rule:nonzero;"/>
|
|
||||||
<path d="M37.411,48.156L39.974,48.156C40.927,48.156 41.661,47.578 41.864,46.641L42.552,43.812C42.942,43.656 43.317,43.516 43.661,43.344L46.145,44.875C46.942,45.375 47.911,45.297 48.552,44.625L50.317,42.844C50.989,42.172 51.083,41.188 50.567,40.391L49.067,37.938C49.224,37.594 49.38,37.234 49.489,36.875L52.349,36.203C53.27,35.984 53.864,35.25 53.864,34.266L53.864,31.781C53.864,30.828 53.27,30.094 52.349,29.891L49.52,29.219C49.395,28.812 49.224,28.438 49.083,28.109L50.599,25.625C51.114,24.812 51.036,23.859 50.349,23.188L48.536,21.406C47.88,20.797 46.989,20.641 46.192,21.109L43.661,22.672C43.317,22.516 42.974,22.359 42.567,22.219L41.864,19.344C41.645,18.406 40.927,17.828 39.974,17.828L37.411,17.828C36.458,17.828 35.739,18.406 35.52,19.359L34.833,22.188C34.442,22.328 34.052,22.484 33.692,22.656L31.192,21.109C30.38,20.641 29.505,20.766 28.833,21.406L27.02,23.188C26.349,23.859 26.27,24.812 26.77,25.625L28.286,28.109C28.145,28.438 28.005,28.812 27.849,29.219L25.02,29.891C24.099,30.094 23.52,30.828 23.52,31.781L23.52,34.266C23.52,35.25 24.099,35.984 25.02,36.203L27.88,36.875C28.02,37.234 28.145,37.594 28.302,37.938L26.833,40.391C26.302,41.188 26.395,42.172 27.067,42.844L28.817,44.625C29.489,45.297 30.442,45.375 31.239,44.875L33.708,43.344C34.067,43.516 34.442,43.656 34.833,43.812L35.52,46.641C35.739,47.578 36.458,48.156 37.411,48.156ZM38.692,37.797C36.02,37.797 33.849,35.641 33.849,32.969C33.849,30.312 36.02,28.125 38.692,28.125C41.349,28.125 43.52,30.312 43.52,32.969C43.52,35.641 41.349,37.797 38.692,37.797Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 499 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 357 KiB |
@ -39,9 +39,6 @@
|
|||||||
.ant-splitter,
|
.ant-splitter,
|
||||||
.ant-steps-item-title,
|
.ant-steps-item-title,
|
||||||
.ant-steps-icon,
|
.ant-steps-icon,
|
||||||
.ant-color-picker-presets-label,
|
|
||||||
.ant-select-selection-item,
|
|
||||||
.ant-color-picker-trigger-text,
|
|
||||||
[class*=' ant-radio'] {
|
[class*=' ant-radio'] {
|
||||||
font-family: 'DM Sans';
|
font-family: 'DM Sans';
|
||||||
}
|
}
|
||||||
@ -83,168 +80,28 @@
|
|||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-selection-item .country-display {
|
|
||||||
margin-left: 1px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-display-tag,
|
|
||||||
.object-type-display-tag,
|
|
||||||
.country-display {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-select.ant-select-focused .object-display-tag,
|
|
||||||
.ant-select-focused .object-type-display-tag,
|
|
||||||
.ant-select-focused .country-display {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag {
|
.flag {
|
||||||
line-height: 0.7;
|
line-height: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electron-navigation-wrapper {
|
.electron-navigation-wrapper {
|
||||||
user-select: none;
|
-webkit-app-region: drag;
|
||||||
--webkit-user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.electron-body * {
|
.electron-navigation-wrapper li,
|
||||||
-webkit-user-select: none;
|
.electron-navigation-wrapper button,
|
||||||
-khtml-user-select: none;
|
.electron-navigation-wrapper .ant-tag {
|
||||||
-moz-user-select: none;
|
-webkit-app-region: no-drag;
|
||||||
-o-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.electron-body .ant-descriptions-item-content *,
|
|
||||||
.electron-body .ant-table-cell * {
|
|
||||||
-webkit-user-select: text;
|
|
||||||
-khtml-user-select: text;
|
|
||||||
-moz-user-select: text;
|
|
||||||
-o-user-select: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Native macOS vibrancy — transparent web content over NSVisualEffectView */
|
|
||||||
html.macos-vibrancy {
|
|
||||||
--macos-window-corner-radius: 15px; /* keep in sync with MAC_WINDOW_CORNER_RADIUS */
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy,
|
|
||||||
html.macos-vibrancy body,
|
|
||||||
html.macos-vibrancy #root {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .ant-layout.ant-layout-has-sider,
|
|
||||||
html.macos-vibrancy .main-layout {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .light-mode .ant-layout.main-content-layout {
|
|
||||||
background: rgba(255, 255, 255, 0.88) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .dark-mode .ant-layout.main-content-layout {
|
|
||||||
background: rgba(0, 0, 0, 0.88) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .light-mode .ant-layout-sider {
|
|
||||||
background: rgba(244, 244, 244, 0.82) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .dark-mode .ant-layout-sider {
|
|
||||||
background: rgba(10, 10, 10, 0.82) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .light-mode .ant-layout-content {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .dark-mode .ant-layout-content {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .ant-layout-sider-light,
|
|
||||||
html.macos-vibrancy .electron-sider .ant-menu-light {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy
|
|
||||||
.dark-mode
|
|
||||||
.electron-sider
|
|
||||||
.ant-menu-light
|
|
||||||
.ant-menu-item-selected {
|
|
||||||
background-color: color-mix(in srgb, var(--color-primary) 30%, #00000010);
|
|
||||||
color: color-mix(in srgb, var(--color-primary) 75%, #ffffff);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy
|
|
||||||
.light-mode
|
|
||||||
.electron-sider
|
|
||||||
.ant-menu-light
|
|
||||||
.ant-menu-item-selected {
|
|
||||||
background-color: color-mix(in srgb, var(--color-primary) 80%, #ffffff3d);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .dark-mode .electron-navigation-wrapper {
|
|
||||||
background: rgba(10, 10, 10, 0.9) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .light-mode .electron-navigation-wrapper {
|
|
||||||
background: rgba(255, 255, 255, 0.9) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .electron-navigation {
|
|
||||||
background: rgba(0, 0, 0, 0) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.macos-vibrancy .light-mode .ant-layout.main-content-layout .ant-card {
|
|
||||||
background: rgba(255, 255, 255, 0.5) !important;
|
|
||||||
}
|
|
||||||
html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card {
|
|
||||||
background: rgba(31, 31, 31, 0.5) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.redTrafficLight {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: #fe5f57;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellowTrafficLight {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: #febc2e;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greenTrafficLight {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: #28c840;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.electron-navigation {
|
.electron-navigation {
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electron-sidebar.ant-menu-inline .ant-menu-item {
|
|
||||||
padding-left: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.electron-navigation .ant-menu-overflow-item-rest {
|
.electron-navigation .ant-menu-overflow-item-rest {
|
||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electron-navigation.ant-menu-horizontal .ant-menu-item {
|
|
||||||
padding-inline: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.electron-sidebar .ant-menu-item,
|
.electron-sidebar .ant-menu-item,
|
||||||
.electron-sidebar .ant-menu-submenu-title {
|
.electron-sidebar .ant-menu-submenu-title {
|
||||||
height: 32.5px !important;
|
height: 32.5px !important;
|
||||||
@ -262,8 +119,6 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card {
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--unit-100vh: 100vh;
|
--unit-100vh: 100vh;
|
||||||
--scrollbox-vertical-right-padding: 16px;
|
|
||||||
--scrollbox-horizontal-bottom-padding: 16px;
|
|
||||||
}
|
}
|
||||||
@supports (height: 100dvh) {
|
@supports (height: 100dvh) {
|
||||||
:root {
|
:root {
|
||||||
@ -341,7 +196,6 @@ code {
|
|||||||
/* --- Start of src/index.css --- */
|
/* --- Start of src/index.css --- */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-mask {
|
.ant-modal-mask {
|
||||||
@ -404,11 +258,6 @@ body {
|
|||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter.large > .ant-splitter-bar {
|
|
||||||
margin: 11px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-mode {
|
.dark-mode {
|
||||||
--sb-track-color: #1d1d1d;
|
--sb-track-color: #1d1d1d;
|
||||||
--sb-thumb-color: #848484;
|
--sb-thumb-color: #848484;
|
||||||
@ -482,36 +331,15 @@ body {
|
|||||||
margin-left: 1px !important;
|
margin-left: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-input.ant-select-multiple .ant-select-selection-item,
|
|
||||||
.tags-input.ant-tree-select.ant-select-multiple .ant-select-selection-item {
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin-inline-end: 0 !important;
|
|
||||||
height: auto !important;
|
|
||||||
line-height: normal !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-input.ant-select-multiple .ant-select-selection-item-remove,
|
|
||||||
.tags-input.ant-tree-select.ant-select-multiple
|
|
||||||
.ant-select-selection-item-remove {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-badge.ant-badge-status {
|
.ant-badge.ant-badge-status {
|
||||||
line-height: 18.5px;
|
line-height: 18.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simplebar-track.simplebar-vertical {
|
.simplebar-track.simplebar-vertical {
|
||||||
right: calc(-1 * var(--scrollbox-vertical-right-padding));
|
right: -16px;
|
||||||
width: 8px !important;
|
width: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simplebar-track.simplebar-horizontal {
|
|
||||||
bottom: calc(-1 * var(--scrollbox-horizontal-bottom-padding));
|
|
||||||
height: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simplebar-scrollbar:before {
|
.simplebar-scrollbar:before {
|
||||||
background: #78787854 !important;
|
background: #78787854 !important;
|
||||||
}
|
}
|
||||||
@ -541,7 +369,7 @@ body {
|
|||||||
margin-right: 1px !important;
|
margin-right: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-select .ant-select-selection-item .object-display-tag {
|
.ant-select-selection-item .object-display-tag {
|
||||||
top: 1.5px;
|
top: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,7 +531,7 @@ body {
|
|||||||
.markdown p {
|
.markdown p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
line-height: 1.6;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h1 {
|
.markdown h1 {
|
||||||
@ -726,10 +554,6 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-changes .markdown {
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown ul,
|
.markdown ul,
|
||||||
.markdown ol {
|
.markdown ol {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
@ -763,7 +587,6 @@ body {
|
|||||||
|
|
||||||
.h-100 {
|
.h-100 {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table.ant-table-middle .ant-table-filter-trigger {
|
.ant-table.ant-table-middle .ant-table-filter-trigger {
|
||||||
@ -773,316 +596,3 @@ body {
|
|||||||
.ant-table-wrapper .ant-table-filter-column {
|
.ant-table-wrapper .ant-table-filter-column {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|
||||||
width: 50px;
|
|
||||||
min-width: 0;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
|
||||||
right: -1px;
|
|
||||||
width: 0px !important;
|
|
||||||
transition:
|
|
||||||
width 0.15s ease-in-out,
|
|
||||||
right 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
|
||||||
border-radius: 0 0 6.5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.dark .simplebar-track.simplebar-vertical {
|
|
||||||
background-color: #141414;
|
|
||||||
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
|
||||||
border-bottom: 1px solid rgba(253, 253, 253, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.light .simplebar-track.simplebar-vertical {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-inline-end: 1px solid rgba(5, 5, 5, 0.06);
|
|
||||||
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.simplebar-scrolling .simplebar-track.simplebar-vertical {
|
|
||||||
width: 12px !important;
|
|
||||||
right: -13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll
|
|
||||||
.simplebar-track.simplebar-vertical
|
|
||||||
.simplebar-scrollbar:before {
|
|
||||||
top: 4px;
|
|
||||||
bottom: 4px;
|
|
||||||
left: 4px;
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker
|
|
||||||
.ant-color-picker-inner
|
|
||||||
.ant-color-picker-panel
|
|
||||||
.ant-color-picker-select
|
|
||||||
.ant-color-picker-palette {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker
|
|
||||||
.ant-collapse
|
|
||||||
.ant-collapse-item:last-child
|
|
||||||
.ant-collapse-content-box {
|
|
||||||
padding-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker .ant-collapse .ant-collapse-expand-icon {
|
|
||||||
height: 12px !important;
|
|
||||||
padding-inline-end: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker .ant-color-picker-inner .ant-color-picker-presets-label {
|
|
||||||
line-height: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker
|
|
||||||
.ant-collapse
|
|
||||||
.ant-collapse-item:first-child
|
|
||||||
.ant-collapse-header {
|
|
||||||
margin: 0 0 5.5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-color-picker .ant-collapse .ant-collapse-item .ant-collapse-header {
|
|
||||||
margin: 5.5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .ant-timeline-item-last > .ant-timeline-item-content {
|
|
||||||
min-height: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .ant-timeline-item-last {
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-steps.ant-steps-vertical
|
|
||||||
> .ant-steps-item
|
|
||||||
> .ant-steps-item-container
|
|
||||||
> .ant-steps-item-tail::after {
|
|
||||||
width: 1.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .ant-timeline-item-head {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .subSteps-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .subSteps-dot svg path {
|
|
||||||
stroke: var(--layout-modal-bg);
|
|
||||||
stroke-width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .subSteps-dot span {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .subSteps-dot.subSteps-dot-disabled {
|
|
||||||
background-color: var(--color-primary-disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subSteps.ant-timeline .subSteps-dot.subSteps-dot-disabled svg path {
|
|
||||||
stroke: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-steps .ant-steps-item-finish .ant-steps-item-icon svg path {
|
|
||||||
stroke: var(--color-primary);
|
|
||||||
stroke-width: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-descriptions .ant-descriptions-item-label::after {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-descriptions-small .ant-descriptions-row > td {
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimal-code-block-editor .ant-typography pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-splitter-panel:not(:first-child) .info-collapse .info-collapse-label {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.farmcontrol-splitter .ant-splitter-panel {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.farmcontrol-splitter
|
|
||||||
.ant-splitter-panel:has(~ .ant-splitter-panel)
|
|
||||||
.info-collapse.ant-collapse.ant-collapse-icon-position-end
|
|
||||||
> .ant-collapse-item
|
|
||||||
> .ant-collapse-header
|
|
||||||
.ant-collapse-expand-icon {
|
|
||||||
padding-inline-end: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.objectTableCards {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.objectTableCardsContainer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.objectTableCardsContainer .ant-spin-nested-loading {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.objectTableCardsContainer .ant-spin-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.electron-body .ant-modal-wrap,
|
|
||||||
.electron-body .ant-modal-mask {
|
|
||||||
top: 41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes h-progress-macos-wave {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
/* One gradient period (40cqw of a 200cqw-wide element) for a seamless loop */
|
|
||||||
transform: translateX(20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-outer {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-inner {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
vertical-align: middle;
|
|
||||||
/* Lets the wave size itself against the full track via cqw units */
|
|
||||||
container-type: inline-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-bg,
|
|
||||||
.h-progress-success-bg {
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.1s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-success-bg {
|
|
||||||
position: absolute;
|
|
||||||
inset-block-start: 0;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mask that clips the wave to the filled portion of the bar. Its width
|
|
||||||
tracks the fill, while the wave inside keeps a constant width so Safari
|
|
||||||
doesn't restart/freeze the animation when progress updates. */
|
|
||||||
.h-progress-bg-mask {
|
|
||||||
position: absolute;
|
|
||||||
inset-block: 0;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: width 0.1s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-bg-wave {
|
|
||||||
position: absolute;
|
|
||||||
inset-block: 0;
|
|
||||||
/* Constant width relative to the full track (.h-progress-inner),
|
|
||||||
independent of the mask's changing width. Starts one period early so
|
|
||||||
the pattern always covers the track while sliding right. */
|
|
||||||
inset-inline-start: -40cqw;
|
|
||||||
width: 200cqw;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0) 0,
|
|
||||||
rgba(255, 255, 255, 0.35) 20cqw,
|
|
||||||
rgba(255, 255, 255, 0) 40cqw
|
|
||||||
);
|
|
||||||
animation: h-progress-macos-wave 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-text {
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
line-height: 1;
|
|
||||||
width: 2em;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: start;
|
|
||||||
vertical-align: middle;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-text-start {
|
|
||||||
width: max-content;
|
|
||||||
margin-inline-start: 0;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-text-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin-inline-start: 0;
|
|
||||||
padding: 0 4px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-layout-bottom {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-progress-layout-bottom .h-progress-text {
|
|
||||||
width: max-content;
|
|
||||||
margin-inline-start: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(241,0,27);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(255,92,96);"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 804 B |
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(202,2,24);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(212,76,80);"/>
|
|
||||||
<g>
|
|
||||||
<path d="M22.5,57.8L57.8,22.5C59.2,21.1 61.4,21.1 62.8,22.5L62.9,22.6C64.3,24 64.3,26.2 62.9,27.6L27.6,62.9C26.2,64.3 24,64.3 22.6,62.9L22.5,62.8C21.2,61.4 21.2,59.2 22.5,57.8Z" style="fill:rgb(128,47,49);stroke:rgb(128,47,49);stroke-width:2.5px;"/>
|
|
||||||
<path d="M27.6,22.5L62.9,57.8C64.3,59.2 64.3,61.4 62.9,62.8L62.8,62.9C61.4,64.3 59.2,64.3 57.8,62.9L22.5,27.6C21.1,26.2 21.1,24 22.5,22.6L22.6,22.5C24,21.2 26.2,21.2 27.6,22.5Z" style="fill:rgb(128,47,49);stroke:rgb(128,47,49);stroke-width:2.5px;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(241,0,27);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(255,92,96);"/>
|
|
||||||
<g>
|
|
||||||
<path d="M22.5,57.8L57.8,22.5C59.2,21.1 61.4,21.1 62.8,22.5L62.9,22.6C64.3,24 64.3,26.2 62.9,27.6L27.6,62.9C26.2,64.3 24,64.3 22.6,62.9L22.5,62.8C21.2,61.4 21.2,59.2 22.5,57.8Z" style="fill:rgb(128,47,49);stroke:rgb(128,47,49);stroke-width:2.5px;"/>
|
|
||||||
<path d="M27.6,22.5L62.9,57.8C64.3,59.2 64.3,61.4 62.9,62.8L62.8,62.9C61.4,64.3 59.2,64.3 57.8,62.9L22.5,27.6C21.1,26.2 21.1,24 22.5,22.6L22.6,22.5C24,21.2 26.2,21.2 27.6,22.5Z" style="fill:rgb(128,47,49);stroke:rgb(128,47,49);stroke-width:2.5px;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(66,130,52);"/>
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.2 21.1,81.8 42.7,81.8Z" style="fill:rgb(46,164,75);"/>
|
|
||||||
<g transform="matrix(0,-0.842628,0.842628,0,6.811922,78.99493)">
|
|
||||||
<g transform="matrix(-1,0,-0,-1,106.637317,63.746683)">
|
|
||||||
<path d="M28.052,20.855L57.9,20.8C61.5,20.8 64.4,23.7 64.4,27.3L64.444,57.088L28.052,20.855Z" style="fill:rgb(17,49,7);"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(-1,0,-0,-1,63.293317,107.454683)">
|
|
||||||
<path d="M56.994,64.5L27.6,64.5C24,64.5 21.1,61.6 21.1,58L21.1,28.154L56.994,64.5Z" style="fill:rgb(17,49,7);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(0,183,30);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.2 21.1,81.8 42.7,81.8Z" style="fill:rgb(54,198,89);"/>
|
|
||||||
<g transform="matrix(0,-0.842628,0.842628,0,6.811922,78.99493)">
|
|
||||||
<g>
|
|
||||||
<g transform="matrix(-1,0,0,-1,106.637317,63.746683)">
|
|
||||||
<path d="M28.052,20.855L57.9,20.8C61.5,20.8 64.4,23.7 64.4,27.3L64.444,57.088L28.052,20.855Z" style="fill:rgb(1,97,0);"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(-1,0,0,-1,63.293317,107.454683)">
|
|
||||||
<path d="M56.994,64.5L27.6,64.5C24,64.5 21.1,61.6 21.1,58L21.1,28.154L56.994,64.5Z" style="fill:rgb(1,97,0);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(0,183,30);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.2 21.1,81.8 42.7,81.8Z" style="fill:rgb(54,198,89);"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 804 B |
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(66,130,52);"/>
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.2 21.1,81.8 42.7,81.8Z" style="fill:rgb(46,164,75);"/>
|
|
||||||
<g transform="matrix(0,-0.842628,0.842628,0,6.811922,78.99493)">
|
|
||||||
<path d="M28.052,20.855L57.9,20.8C61.5,20.8 64.4,23.7 64.4,27.3L64.444,57.088L28.052,20.855ZM56.994,64.5L27.6,64.5C24,64.5 21.1,61.6 21.1,58L21.1,28.154L56.994,64.5Z" style="fill:rgb(17,49,7);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(0,183,30);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.2 21.1,81.8 42.7,81.8Z" style="fill:rgb(54,198,89);"/>
|
|
||||||
<g transform="matrix(0,-0.842628,0.842628,0,6.811922,78.99493)">
|
|
||||||
<path d="M28.052,20.855L57.9,20.8C61.5,20.8 64.4,23.7 64.4,27.3L64.444,57.088L28.052,20.855ZM56.994,64.5L27.6,64.5C24,64.5 21.1,61.6 21.1,58L21.1,28.154L56.994,64.5Z" style="fill:rgb(17,49,7);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(239,174,0);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(250,200,0);"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 805 B |
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(200,146,0);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(208,165,5);"/>
|
|
||||||
<path d="M22.154,39.1L63.792,39.1C65.692,39.1 67.292,40.7 67.292,42.6L67.292,42.7C67.292,44.6 65.692,46.2 63.792,46.2L22.154,46.2C20.254,46.2 18.654,44.6 18.654,42.7L18.654,42.6C18.654,40.7 20.154,39.1 22.154,39.1Z" style="fill:rgb(126,100,12);stroke:rgb(126,100,12);stroke-width:3.5px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(239,174,0);"/>
|
|
||||||
<path d="M42.7,81.8C64.3,81.8 81.8,64.3 81.8,42.7C81.8,21.1 64.3,3.6 42.7,3.6C21.1,3.6 3.6,21.1 3.6,42.7C3.6,64.3 21.1,81.8 42.7,81.8Z" style="fill:rgb(250,200,0);"/>
|
|
||||||
<path d="M22.154,39.1L63.792,39.1C65.692,39.1 67.292,40.7 67.292,42.6L67.292,42.7C67.292,44.6 65.692,46.2 63.792,46.2L22.154,46.2C20.254,46.2 18.654,44.6 18.654,42.7L18.654,42.6C18.654,40.7 20.154,39.1 22.154,39.1Z" style="fill:rgb(126,100,12);stroke:rgb(126,100,12);stroke-width:3.5px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 86 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g>
|
|
||||||
<path d="M42.7,85.4C66.3,85.4 85.4,66.3 85.4,42.7C85.4,19.1 66.3,0 42.7,0C19.1,0 0,19.1 0,42.7C0,66.3 19.1,85.4 42.7,85.4Z" style="fill:rgb(209,208,210);fill-opacity:0.3;"/>
|
|
||||||
<path d="M42.7,81.7C64.3,81.7 81.8,64.2 81.8,42.6C81.8,21 64.3,3.5 42.7,3.5C21.1,3.5 3.6,21 3.6,42.6C3.6,64.2 21.1,81.7 42.7,81.7Z" style="fill:rgb(199,199,199);fill-opacity:0.3;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 839 B |
|
Before Width: | Height: | Size: 279 KiB |
@ -1,88 +0,0 @@
|
|||||||
import type { ElectrobunConfig } from "electrobun";
|
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
const packageJson = JSON.parse(
|
|
||||||
readFileSync(path.join(rootDir, "package.json"), "utf8"),
|
|
||||||
);
|
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
|
|
||||||
const isStable = buildEnv === "stable";
|
|
||||||
const canCodesign = Boolean(process.env.ELECTROBUN_DEVELOPER_ID);
|
|
||||||
const bundleCEF = ["1", "true", "yes"].includes(
|
|
||||||
String(process.env.ELECTROBUN_BUNDLE_CEF || "").toLowerCase(),
|
|
||||||
);
|
|
||||||
const defaultRenderer = bundleCEF ? "cef" : "native";
|
|
||||||
const targetOs =
|
|
||||||
process.env.ELECTROBUN_OS ||
|
|
||||||
(process.platform === "darwin"
|
|
||||||
? "macos"
|
|
||||||
: process.platform === "win32"
|
|
||||||
? "win"
|
|
||||||
: "linux");
|
|
||||||
|
|
||||||
export default {
|
|
||||||
app: {
|
|
||||||
name: "Farm Control",
|
|
||||||
identifier: "com.tombutcher.farmcontrol",
|
|
||||||
version: packageJson.version,
|
|
||||||
description: "3D Printer ERP and Control Software.",
|
|
||||||
urlSchemes: ["farmcontrol"],
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
exitOnLastWindowClosed: true,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
buildFolder: "build",
|
|
||||||
artifactFolder: "app_dist",
|
|
||||||
// macOS WebKit loads views:// assets reliably from a flat app folder;
|
|
||||||
// ASAR-packed views can fail to load module scripts on first launch.
|
|
||||||
useAsar: isStable && targetOs !== "macos",
|
|
||||||
asarUnpack: [
|
|
||||||
"*.node",
|
|
||||||
"*.dll",
|
|
||||||
"*.dylib",
|
|
||||||
"*.so",
|
|
||||||
"views/**",
|
|
||||||
],
|
|
||||||
bun: {
|
|
||||||
entrypoint: "src/bun/index.js",
|
|
||||||
},
|
|
||||||
copy: {
|
|
||||||
"dist/mainview": "views/mainview",
|
|
||||||
"src/bun/libMacWindowEffects.dylib": "bun/libMacWindowEffects.dylib",
|
|
||||||
},
|
|
||||||
watch: ["scripts", "src"],
|
|
||||||
watchIgnore: ["dist/**", "build/**", "app_dist/**"],
|
|
||||||
mac: {
|
|
||||||
bundleCEF,
|
|
||||||
defaultRenderer,
|
|
||||||
icons: "assets/icon.iconset",
|
|
||||||
createDmg: false,
|
|
||||||
codesign: isStable && canCodesign,
|
|
||||||
notarize: isStable && canCodesign,
|
|
||||||
},
|
|
||||||
linux: {
|
|
||||||
bundleCEF: false,
|
|
||||||
defaultRenderer: "native",
|
|
||||||
icon: "assets/icon.png",
|
|
||||||
},
|
|
||||||
win: {
|
|
||||||
bundleCEF,
|
|
||||||
defaultRenderer,
|
|
||||||
icon: "assets/icon.iconset/icon_256x256.png",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
preBuild: "scripts/pre-build.mjs",
|
|
||||||
postWrap: "scripts/expand-macos-bundle.mjs",
|
|
||||||
postPackage: "scripts/finalize-desktop-artifacts.mjs",
|
|
||||||
},
|
|
||||||
release: {
|
|
||||||
baseUrl:
|
|
||||||
process.env.ELECTROBUN_RELEASE_BASE_URL ||
|
|
||||||
process.env.RELEASE_BASE_URL ||
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
} satisfies ElectrobunConfig;
|
|
||||||
@ -1,515 +0,0 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <objc/runtime.h>
|
|
||||||
|
|
||||||
static NSString *const kElectrobunVibrancyViewIdentifier =
|
|
||||||
@"ElectrobunVibrancyView";
|
|
||||||
static NSString *const kElectrobunNativeDragViewIdentifier =
|
|
||||||
@"ElectrobunNativeDragView";
|
|
||||||
static NSString *const kElectrobunWindowBorderViewIdentifier =
|
|
||||||
@"ElectrobunWindowBorderView";
|
|
||||||
|
|
||||||
static CGFloat gWindowCornerRadius = 15.0;
|
|
||||||
static CGFloat gTrafficLightsX = 16.0;
|
|
||||||
static CGFloat gTrafficLightsY = 12.0;
|
|
||||||
|
|
||||||
static const void *kElectrobunChromeObserverKey = &kElectrobunChromeObserverKey;
|
|
||||||
|
|
||||||
@interface ElectrobunWindowBorderView : NSView
|
|
||||||
@property(nonatomic) CGFloat cornerRadius;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation ElectrobunWindowBorderView
|
|
||||||
@synthesize cornerRadius = _cornerRadius;
|
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
||||||
self = [super initWithFrame:frameRect];
|
|
||||||
if (self) {
|
|
||||||
_cornerRadius = gWindowCornerRadius;
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)isOpaque {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)acceptsFirstMouse:(NSEvent *)event {
|
|
||||||
(void)event;
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSView *)hitTest:(NSPoint)point {
|
|
||||||
(void)point;
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setFrame:(NSRect)frame {
|
|
||||||
[super setFrame:frame];
|
|
||||||
[self setNeedsDisplay:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)drawRect:(NSRect)dirtyRect {
|
|
||||||
(void)dirtyRect;
|
|
||||||
|
|
||||||
NSRect bounds = [self bounds];
|
|
||||||
CGFloat radius = MAX(0.0, self.cornerRadius);
|
|
||||||
NSBezierPath *borderPath = [NSBezierPath
|
|
||||||
bezierPathWithRoundedRect:NSInsetRect(bounds, 0.5, 0.5)
|
|
||||||
xRadius:radius
|
|
||||||
yRadius:radius];
|
|
||||||
|
|
||||||
if (@available(macOS 10.14, *)) {
|
|
||||||
NSAppearance *appearance = [self effectiveAppearance];
|
|
||||||
BOOL isDark =
|
|
||||||
[[appearance bestMatchFromAppearancesWithNames:@[
|
|
||||||
NSAppearanceNameAqua, NSAppearanceNameDarkAqua
|
|
||||||
]] isEqualToString:NSAppearanceNameDarkAqua];
|
|
||||||
|
|
||||||
if (isDark) {
|
|
||||||
[[NSColor colorWithWhite:1.0 alpha:0.18] setStroke];
|
|
||||||
} else {
|
|
||||||
[[NSColor colorWithWhite:1.0 alpha:0.72] setStroke];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
[[NSColor colorWithWhite:1.0 alpha:0.72] setStroke];
|
|
||||||
}
|
|
||||||
|
|
||||||
borderPath.lineWidth = 1.0;
|
|
||||||
[borderPath stroke];
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface ElectrobunNativeDragView : NSView
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation ElectrobunNativeDragView
|
|
||||||
- (BOOL)isOpaque {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)drawRect:(NSRect)dirtyRect {
|
|
||||||
(void)dirtyRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseDown:(NSEvent *)event {
|
|
||||||
NSWindow *window = [self window];
|
|
||||||
if (window != nil && event != nil) {
|
|
||||||
[window performWindowDragWithEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface ElectrobunWindowChromeObserver : NSObject
|
|
||||||
@property(nonatomic, weak) NSWindow *window;
|
|
||||||
@end
|
|
||||||
|
|
||||||
static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x,
|
|
||||||
CGFloat yFromTop);
|
|
||||||
static void refreshWindowChrome(NSWindow *window);
|
|
||||||
static BOOL isWindowFullScreen(NSWindow *window);
|
|
||||||
|
|
||||||
@implementation ElectrobunWindowChromeObserver
|
|
||||||
|
|
||||||
- (instancetype)initWithWindow:(NSWindow *)window {
|
|
||||||
self = [super init];
|
|
||||||
if (self) {
|
|
||||||
_window = window;
|
|
||||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowDidResizeNotification
|
|
||||||
object:window];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowDidEndLiveResizeNotification
|
|
||||||
object:window];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowDidEnterFullScreenNotification
|
|
||||||
object:window];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowDidExitFullScreenNotification
|
|
||||||
object:window];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowDidBecomeKeyNotification
|
|
||||||
object:window];
|
|
||||||
[center addObserver:self
|
|
||||||
selector:@selector(refreshChrome)
|
|
||||||
name:NSWindowWillEnterFullScreenNotification
|
|
||||||
object:window];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)refreshChrome {
|
|
||||||
if (self.window != nil) {
|
|
||||||
refreshWindowChrome(self.window);
|
|
||||||
applyTrafficLightsPosition(self.window, gTrafficLightsX, gTrafficLightsY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
static NSVisualEffectView *findVibrancyView(NSView *contentView) {
|
|
||||||
for (NSView *subview in [contentView subviews]) {
|
|
||||||
if ([subview isKindOfClass:[NSVisualEffectView class]] &&
|
|
||||||
[[subview identifier]
|
|
||||||
isEqualToString:kElectrobunVibrancyViewIdentifier]) {
|
|
||||||
return (NSVisualEffectView *)subview;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ElectrobunNativeDragView *findNativeDragView(NSView *contentView) {
|
|
||||||
for (NSView *subview in [contentView subviews]) {
|
|
||||||
if ([subview isKindOfClass:[ElectrobunNativeDragView class]] &&
|
|
||||||
[[subview identifier]
|
|
||||||
isEqualToString:kElectrobunNativeDragViewIdentifier]) {
|
|
||||||
return (ElectrobunNativeDragView *)subview;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ElectrobunWindowBorderView *findWindowBorderView(NSView *contentView) {
|
|
||||||
for (NSView *subview in [contentView subviews]) {
|
|
||||||
if ([subview isKindOfClass:[ElectrobunWindowBorderView class]] &&
|
|
||||||
[[subview identifier]
|
|
||||||
isEqualToString:kElectrobunWindowBorderViewIdentifier]) {
|
|
||||||
return (ElectrobunWindowBorderView *)subview;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL isWindowFullScreen(NSWindow *window) {
|
|
||||||
return (window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void refreshWindowChrome(NSWindow *window) {
|
|
||||||
BOOL fullScreen = isWindowFullScreen(window);
|
|
||||||
CGFloat radius = fullScreen ? 0.0 : gWindowCornerRadius;
|
|
||||||
|
|
||||||
NSView *contentView = [window contentView];
|
|
||||||
if (contentView == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
contentView.wantsLayer = YES;
|
|
||||||
contentView.layer.cornerRadius = radius;
|
|
||||||
contentView.layer.masksToBounds = YES;
|
|
||||||
|
|
||||||
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
|
||||||
if (effectView != nil) {
|
|
||||||
effectView.wantsLayer = YES;
|
|
||||||
effectView.layer.cornerRadius = radius;
|
|
||||||
effectView.layer.masksToBounds = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
|
||||||
if (borderView != nil) {
|
|
||||||
borderView.hidden = fullScreen;
|
|
||||||
if (!fullScreen) {
|
|
||||||
borderView.cornerRadius = gWindowCornerRadius;
|
|
||||||
[borderView setNeedsDisplay:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
|
||||||
gWindowCornerRadius = MAX(0.0, radius);
|
|
||||||
refreshWindowChrome(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ensureWindowBorder(NSWindow *window) {
|
|
||||||
NSView *contentView = [window contentView];
|
|
||||||
if (contentView == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
|
||||||
if (borderView == nil) {
|
|
||||||
borderView = [[ElectrobunWindowBorderView alloc]
|
|
||||||
initWithFrame:[contentView bounds]];
|
|
||||||
[borderView setIdentifier:kElectrobunWindowBorderViewIdentifier];
|
|
||||||
[borderView
|
|
||||||
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
|
|
||||||
}
|
|
||||||
|
|
||||||
[borderView setFrame:[contentView bounds]];
|
|
||||||
|
|
||||||
if ([borderView superview] == nil) {
|
|
||||||
[contentView addSubview:borderView
|
|
||||||
positioned:NSWindowAbove
|
|
||||||
relativeTo:nil];
|
|
||||||
} else {
|
|
||||||
[borderView removeFromSuperview];
|
|
||||||
[contentView addSubview:borderView
|
|
||||||
positioned:NSWindowAbove
|
|
||||||
relativeTo:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshWindowChrome(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x,
|
|
||||||
CGFloat yFromTop) {
|
|
||||||
NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton];
|
|
||||||
NSButton *minimizeButton =
|
|
||||||
[window standardWindowButton:NSWindowMiniaturizeButton];
|
|
||||||
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
|
|
||||||
|
|
||||||
if (closeButton == nil || minimizeButton == nil || zoomButton == nil) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSView *buttonContainer = [closeButton superview];
|
|
||||||
if (buttonContainer == nil) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat spacing = NSMinX(minimizeButton.frame) - NSMinX(closeButton.frame);
|
|
||||||
if (spacing <= 0) {
|
|
||||||
spacing = closeButton.frame.size.width + 6.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL flipped = [buttonContainer isFlipped];
|
|
||||||
CGFloat targetY = yFromTop;
|
|
||||||
if (!flipped) {
|
|
||||||
targetY = buttonContainer.frame.size.height - yFromTop -
|
|
||||||
closeButton.frame.size.height;
|
|
||||||
}
|
|
||||||
targetY = MAX(0.0, targetY);
|
|
||||||
|
|
||||||
CGFloat currentX = x;
|
|
||||||
NSArray *buttons = @[ closeButton, minimizeButton, zoomButton ];
|
|
||||||
for (NSButton *button in buttons) {
|
|
||||||
[button setAutoresizingMask:NSViewNotSizable];
|
|
||||||
[button setFrameOrigin:NSMakePoint(currentX, targetY)];
|
|
||||||
currentX += spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ensureTrafficLightsObserver(NSWindow *window) {
|
|
||||||
ElectrobunWindowChromeObserver *existingObserver =
|
|
||||||
objc_getAssociatedObject(window, kElectrobunChromeObserverKey);
|
|
||||||
if (existingObserver != nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElectrobunWindowChromeObserver *observer =
|
|
||||||
[[ElectrobunWindowChromeObserver alloc] initWithWindow:window];
|
|
||||||
objc_setAssociatedObject(window, kElectrobunChromeObserverKey, observer,
|
|
||||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void scheduleTrafficLightsPosition(NSWindow *window, NSInteger attempt) {
|
|
||||||
if (applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt >= 10) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch_after(
|
|
||||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)),
|
|
||||||
dispatch_get_main_queue(), ^{
|
|
||||||
scheduleTrafficLightsPosition(window, attempt + 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
|
|
||||||
if (windowPtr == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block BOOL success = NO;
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
NSWindow *window = (__bridge NSWindow *)windowPtr;
|
|
||||||
if (![window isKindOfClass:[NSWindow class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyWindowCornerRadius(window, (CGFloat)cornerRadius);
|
|
||||||
|
|
||||||
[window setOpaque:NO];
|
|
||||||
[window setBackgroundColor:[NSColor clearColor]];
|
|
||||||
[window setTitlebarAppearsTransparent:YES];
|
|
||||||
[window setHasShadow:YES];
|
|
||||||
|
|
||||||
NSView *contentView = [window contentView];
|
|
||||||
if (contentView == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
|
||||||
|
|
||||||
if (effectView == nil) {
|
|
||||||
effectView = [[NSVisualEffectView alloc]
|
|
||||||
initWithFrame:[contentView bounds]];
|
|
||||||
[effectView setIdentifier:kElectrobunVibrancyViewIdentifier];
|
|
||||||
[effectView
|
|
||||||
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@available(macOS 10.14, *)) {
|
|
||||||
[effectView setMaterial:NSVisualEffectMaterialHUDWindow];
|
|
||||||
} else {
|
|
||||||
[effectView setMaterial:NSVisualEffectMaterialSidebar];
|
|
||||||
}
|
|
||||||
[effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
|
|
||||||
[effectView setState:NSVisualEffectStateActive];
|
|
||||||
|
|
||||||
if ([effectView superview] == nil) {
|
|
||||||
NSView *relativeView = [[contentView subviews] firstObject];
|
|
||||||
if (relativeView != nil) {
|
|
||||||
[contentView addSubview:effectView
|
|
||||||
positioned:NSWindowBelow
|
|
||||||
relativeTo:relativeView];
|
|
||||||
} else {
|
|
||||||
[contentView addSubview:effectView];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureWindowBorder(window);
|
|
||||||
applyWindowCornerRadius(window, gWindowCornerRadius);
|
|
||||||
ensureTrafficLightsObserver(window);
|
|
||||||
scheduleTrafficLightsPosition(window, 0);
|
|
||||||
|
|
||||||
[window invalidateShadow];
|
|
||||||
success = YES;
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" bool setWindowCornerRadius(void *windowPtr, double cornerRadius) {
|
|
||||||
if (windowPtr == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block BOOL success = NO;
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
NSWindow *window = (__bridge NSWindow *)windowPtr;
|
|
||||||
if (![window isKindOfClass:[NSWindow class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyWindowCornerRadius(window, (CGFloat)cornerRadius);
|
|
||||||
ensureWindowBorder(window);
|
|
||||||
[window invalidateShadow];
|
|
||||||
success = YES;
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" bool ensureWindowShadow(void *windowPtr) {
|
|
||||||
if (windowPtr == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block BOOL success = NO;
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
NSWindow *window = (__bridge NSWindow *)windowPtr;
|
|
||||||
if (![window isKindOfClass:[NSWindow class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[window setHasShadow:YES];
|
|
||||||
[window invalidateShadow];
|
|
||||||
ensureWindowBorder(window);
|
|
||||||
applyWindowCornerRadius(window, gWindowCornerRadius);
|
|
||||||
success = YES;
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" bool setWindowTrafficLightsPosition(void *windowPtr, double x,
|
|
||||||
double yFromTop) {
|
|
||||||
if (windowPtr == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block BOOL success = NO;
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
NSWindow *window = (__bridge NSWindow *)windowPtr;
|
|
||||||
if (![window isKindOfClass:[NSWindow class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gTrafficLightsX = (CGFloat)x;
|
|
||||||
gTrafficLightsY = (CGFloat)yFromTop;
|
|
||||||
ensureTrafficLightsObserver(window);
|
|
||||||
success = applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY);
|
|
||||||
if (success) {
|
|
||||||
[window invalidateShadow];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" bool setNativeWindowDragRegion(void *windowPtr, double x,
|
|
||||||
double height) {
|
|
||||||
if (windowPtr == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block BOOL success = NO;
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
NSWindow *window = (__bridge NSWindow *)windowPtr;
|
|
||||||
if (![window isKindOfClass:[NSWindow class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSView *contentView = [window contentView];
|
|
||||||
if (contentView == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat dragX = MAX(0.0, x);
|
|
||||||
CGFloat dragHeight = MAX(0.0, height);
|
|
||||||
CGFloat dragWidth = MAX(0.0, contentView.bounds.size.width - dragX);
|
|
||||||
if (dragHeight <= 0.0 || dragWidth <= 0.0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL flipped = [contentView isFlipped];
|
|
||||||
CGFloat dragY = flipped ? 0.0 : contentView.bounds.size.height - dragHeight;
|
|
||||||
dragY = MAX(0.0, dragY);
|
|
||||||
|
|
||||||
ElectrobunNativeDragView *dragView = findNativeDragView(contentView);
|
|
||||||
if (dragView == nil) {
|
|
||||||
dragView = [[ElectrobunNativeDragView alloc] initWithFrame:NSZeroRect];
|
|
||||||
[dragView setIdentifier:kElectrobunNativeDragViewIdentifier];
|
|
||||||
}
|
|
||||||
|
|
||||||
[dragView setFrame:NSMakeRect(dragX, dragY, dragWidth, dragHeight)];
|
|
||||||
[dragView setAutoresizingMask:NSViewWidthSizable];
|
|
||||||
|
|
||||||
if ([dragView superview] == nil) {
|
|
||||||
[contentView addSubview:dragView
|
|
||||||
positioned:NSWindowAbove
|
|
||||||
relativeTo:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
success = YES;
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
94
package.json
@ -4,11 +4,12 @@
|
|||||||
"name": "Tom Butcher",
|
"name": "Tom Butcher",
|
||||||
"email": "tom@tombutcher.work"
|
"email": "tom@tombutcher.work"
|
||||||
},
|
},
|
||||||
"version": "0.1.1",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/charts": "^2.6.5",
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
@ -25,11 +26,7 @@
|
|||||||
"@codemirror/lang-sql": "^6.9.1",
|
"@codemirror/lang-sql": "^6.9.1",
|
||||||
"@codemirror/lang-xml": "^6.1.0",
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/commands": "6.10.4",
|
|
||||||
"@codemirror/language": "6.12.1",
|
|
||||||
"@codemirror/state": "6.7.1",
|
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "6.39.12",
|
|
||||||
"@simplewebauthn/browser": "^13.1.2",
|
"@simplewebauthn/browser": "^13.1.2",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"@tiptap/extension-link": "^3.20.1",
|
"@tiptap/extension-link": "^3.20.1",
|
||||||
@ -42,23 +39,18 @@
|
|||||||
"@tsparticles/react": "^3.0.0",
|
"@tsparticles/react": "^3.0.0",
|
||||||
"@tsparticles/slim": "^3.9.1",
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"@uiw/react-codemirror": "^4.25.1",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
|
||||||
"antd": "^5.27.1",
|
"antd": "^5.27.1",
|
||||||
"antd-style": "^3.7.1",
|
"antd-style": "^3.7.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"blurhash": "^2.0.5",
|
|
||||||
"codemirror": "^6.0.2",
|
|
||||||
"country-list": "^2.4.1",
|
"country-list": "^2.4.1",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"electron-store": "^11.0.2",
|
|
||||||
"gcode-preview": "^2.18.0",
|
"gcode-preview": "^2.18.0",
|
||||||
"json-schema-traverse": "^1.0.0",
|
|
||||||
"keycloak-js": "^26.2.0",
|
"keycloak-js": "^26.2.0",
|
||||||
|
"keytar": "^7.9.0",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"nanoid": "^5.1.14",
|
|
||||||
"online-3d-viewer": "^0.16.0",
|
"online-3d-viewer": "^0.16.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@ -67,11 +59,13 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-responsive": "^10.0.1",
|
"react-responsive": "^10.0.1",
|
||||||
"react-router-dom": "^7.8.2",
|
"react-router-dom": "^7.8.2",
|
||||||
"recharts": "^3.8.1",
|
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"simplebar-react": "^3.3.2",
|
"simplebar-react": "^3.3.2",
|
||||||
"socket.io-client": "*",
|
"socket.io-client": "*",
|
||||||
|
"standard": "^17.1.2",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
|
"svgo": "^4.0.0",
|
||||||
|
"svgo-loader": "^4.0.0",
|
||||||
"three": "^0.179.1",
|
"three": "^0.179.1",
|
||||||
"tsparticles": "^3.9.1",
|
"tsparticles": "^3.9.1",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
@ -80,21 +74,13 @@
|
|||||||
"description": "3D Printer ERP and Control Software.",
|
"description": "3D Printer ERP and Control Software.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development vite",
|
"dev": "cross-env NODE_ENV=development vite",
|
||||||
"dev:app": "concurrently \"cross-env NODE_ENV=development bun run dev:renderer\" \"cross-env NODE_ENV=development electrobun dev\"",
|
|
||||||
"dev:renderer": "vite --port 5780 --no-open",
|
|
||||||
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
|
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
|
||||||
"start": "serve -s build",
|
"start": "serve -s build",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:app": "electrobun build --env=stable",
|
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 cross-env NODE_ENV=development electron public/electron.js\"",
|
||||||
"build:app:mac": "bun scripts/build-macos.mjs",
|
|
||||||
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --port 5780 --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 NODE_ENV=development electron public/electron.js\"",
|
|
||||||
"build:electron": "vite build && electron-builder",
|
"build:electron": "vite build && electron-builder",
|
||||||
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
||||||
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main",
|
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
|
||||||
"postinstall": "bun scripts/patch-electrobun-src.mjs && bun scripts/build-macos-effects.mjs",
|
|
||||||
"generate-app-icons": "bun scripts/generate-app-icons.mjs",
|
|
||||||
"build:macos-effects": "bun scripts/build-macos-effects.mjs",
|
|
||||||
"clean": "bun scripts/clean-build.mjs"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -118,10 +104,7 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electrobun": "1.18.1",
|
|
||||||
"electron": "^38.7.1",
|
"electron": "^38.7.1",
|
||||||
"jimp": "^1.6.1",
|
|
||||||
"png-to-ico": "^2.1.8",
|
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-packager": "^17.1.2",
|
"electron-packager": "^17.1.2",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
@ -134,7 +117,6 @@
|
|||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-eslint": "^16.4.2",
|
"prettier-eslint": "^16.4.2",
|
||||||
"rcedit": "^4.0.1",
|
|
||||||
"rollup-plugin-visualizer": "^6.0.5",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"serve": "^14.2.4",
|
"serve": "^14.2.4",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
@ -149,7 +131,6 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.tombutcher.farmcontrol",
|
"appId": "com.tombutcher.farmcontrol",
|
||||||
"productName": "Farm Control",
|
"productName": "Farm Control",
|
||||||
"artifactName": "farmcontrol-${version}-${arch}.${ext}",
|
|
||||||
"icon": "assets/logos/farmcontrolicon.png",
|
"icon": "assets/logos/farmcontrolicon.png",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "app_dist"
|
"output": "app_dist"
|
||||||
@ -166,23 +147,11 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"target": "pkg",
|
|
||||||
"arch": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"target": "dmg",
|
"target": "dmg",
|
||||||
"arch": [
|
"arch": [
|
||||||
"x64"
|
"x64"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "pkg",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mergeASARs": true,
|
"mergeASARs": true,
|
||||||
@ -199,45 +168,8 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dmg": {
|
|
||||||
"background": "assets/dmg/background.png",
|
|
||||||
"iconSize": 100,
|
|
||||||
"window": {
|
|
||||||
"width": 540,
|
|
||||||
"height": 380
|
|
||||||
},
|
|
||||||
"contents": [
|
|
||||||
{
|
|
||||||
"x": 130,
|
|
||||||
"y": 220
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 410,
|
|
||||||
"y": 220,
|
|
||||||
"type": "link",
|
|
||||||
"path": "/Applications"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pkg": {
|
|
||||||
"installLocation": "/Applications",
|
|
||||||
"mustClose": [
|
|
||||||
"com.tombutcher.farmcontrol"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": "nsis"
|
||||||
"nsis",
|
|
||||||
"msiWrapped"
|
|
||||||
],
|
|
||||||
"protocols": [
|
|
||||||
{
|
|
||||||
"name": "Farm Control Protocol",
|
|
||||||
"schemes": [
|
|
||||||
"farmcontrol"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": "AppImage"
|
"target": "AppImage"
|
||||||
@ -246,13 +178,7 @@
|
|||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"include": "scripts/installer.nsh",
|
"include": "scripts/installer.nsh",
|
||||||
"perMachine": false
|
"perMachine": true
|
||||||
},
|
|
||||||
"msiWrapped": {
|
|
||||||
"upgradeCode": "{735812DB-E33B-57A0-8FBC-5FC3155925AA}",
|
|
||||||
"perMachine": false,
|
|
||||||
"impersonate": true,
|
|
||||||
"wrappedInstallerArgs": "/S /UPDATE /RESTARTFC"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
!include "MUI2.nsh"
|
|
||||||
!include "LogicLib.nsh"
|
|
||||||
!include "installer.nsh"
|
|
||||||
|
|
||||||
!ifndef OUTFILE
|
|
||||||
!define OUTFILE "farmcontrol-installer.exe"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!ifndef VERSION
|
|
||||||
!define VERSION "0.1.0"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!ifndef BUILD_NUMBER
|
|
||||||
!define BUILD_NUMBER "dev"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!ifndef APP_SOURCE_DIR
|
|
||||||
!define APP_SOURCE_DIR "app"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!ifndef APP_FILE_LIST_GENERATOR
|
|
||||||
!define APP_FILE_LIST_GENERATOR "..\..\scripts\generate-nsis-file-list.ps1"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
; Enumerate APP_SOURCE_DIR now, while compiling, so the generated installer
|
|
||||||
; knows the exact unpacked byte total and can log each extracted file.
|
|
||||||
!insertmacro compileProgressFileCopy "${APP_SOURCE_DIR}" "${APP_FILE_LIST_GENERATOR}"
|
|
||||||
|
|
||||||
Name "Farm Control"
|
|
||||||
OutFile "${OUTFILE}"
|
|
||||||
InstallDir "$LOCALAPPDATA\Programs\Farm Control"
|
|
||||||
InstallDirRegKey HKCU "Software\Tom Butcher\Farm Control" "InstallDir"
|
|
||||||
RequestExecutionLevel user
|
|
||||||
SilentInstall normal
|
|
||||||
|
|
||||||
!define MUI_ABORTWARNING
|
|
||||||
|
|
||||||
!ifndef INSTALLER_ICON
|
|
||||||
!define INSTALLER_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!ifndef UNINSTALLER_ICON
|
|
||||||
!define UNINSTALLER_ICON "${INSTALLER_ICON}"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!define MUI_ICON "${INSTALLER_ICON}"
|
|
||||||
!define MUI_UNICON "${UNINSTALLER_ICON}"
|
|
||||||
|
|
||||||
Icon "${INSTALLER_ICON}"
|
|
||||||
UninstallIcon "${UNINSTALLER_ICON}"
|
|
||||||
|
|
||||||
BrandingText "Farm Control v${VERSION}-b${BUILD_NUMBER} Installer"
|
|
||||||
|
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
|
||||||
!insertmacro MUI_UNPAGE_CONFIRM
|
|
||||||
!insertmacro MUI_UNPAGE_INSTFILES
|
|
||||||
!insertmacro MUI_LANGUAGE "English"
|
|
||||||
|
|
||||||
Function .onInit
|
|
||||||
!insertmacro initProgressLog
|
|
||||||
|
|
||||||
${If} ${Silent}
|
|
||||||
SetAutoClose true
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro progressPhase "Starting installation"
|
|
||||||
!insertmacro progressStatus "Starting Farm Control installation..."
|
|
||||||
!insertmacro progressCopyTotal "${APP_COPY_TOTAL_BYTES}"
|
|
||||||
!insertmacro progressStart
|
|
||||||
|
|
||||||
; In-app updates install into Farm Control.new while the running process
|
|
||||||
; keeps using Farm Control; the folders are swapped after exit (/RESTARTFC).
|
|
||||||
; Interactive / silent fresh installs still remove the previous version first.
|
|
||||||
${If} $IsInAppUpdate == "1"
|
|
||||||
!insertmacro prepareSideBySideUpdate
|
|
||||||
${Else}
|
|
||||||
StrCpy $FinalInstDir $INSTDIR
|
|
||||||
!insertmacro uninstallPreviousFarmControl
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro progressPrepare
|
|
||||||
FunctionEnd
|
|
||||||
|
|
||||||
Function .onInstFailed
|
|
||||||
!insertmacro progressFailure "Installation failed."
|
|
||||||
FunctionEnd
|
|
||||||
|
|
||||||
Function .onInstSuccess
|
|
||||||
Call restartFarmControlAfterUpdate
|
|
||||||
FunctionEnd
|
|
||||||
|
|
||||||
Section "Farm Control" SecMain
|
|
||||||
SectionIn RO
|
|
||||||
|
|
||||||
!insertmacro progressPhase "Copying application files"
|
|
||||||
!insertmacro progressStatus "Copying application files..."
|
|
||||||
!insertmacro progressCopyStart
|
|
||||||
|
|
||||||
SetOverwrite try
|
|
||||||
!insertmacro copyApplicationFilesWithProgress
|
|
||||||
|
|
||||||
!insertmacro progressCopyComplete
|
|
||||||
!insertmacro customInstall
|
|
||||||
|
|
||||||
!insertmacro progressFinalizeInstall
|
|
||||||
!insertmacro progressPhase "Finalizing installation"
|
|
||||||
!insertmacro progressStatus "Writing installation registry entries..."
|
|
||||||
|
|
||||||
WriteRegStr HKCU "Software\Tom Butcher\Farm Control" "InstallDir" $FinalInstDir
|
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"DisplayName" "Farm Control"
|
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"DisplayVersion" "${VERSION}"
|
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"Publisher" "Tom Butcher"
|
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"UninstallString" "$FinalInstDir\Uninstall.exe"
|
|
||||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"NoModify" 1
|
|
||||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
|
|
||||||
"NoRepair" 1
|
|
||||||
|
|
||||||
!insertmacro progressWriteUninstaller
|
|
||||||
!insertmacro progressStatus "Writing uninstall information..."
|
|
||||||
|
|
||||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
|
||||||
|
|
||||||
!insertmacro progressFinalize
|
|
||||||
|
|
||||||
|
|
||||||
; For in-app updates (/RESTARTFC) the success signal is emitted from
|
|
||||||
; restartFarmControlAfterUpdate, right before the install directory swap,
|
|
||||||
; so the running app quits while this installer stays alive.
|
|
||||||
${If} $RestartAfterInstall != "1"
|
|
||||||
!insertmacro progressSuccess
|
|
||||||
${EndIf}
|
|
||||||
SectionEnd
|
|
||||||
|
|
||||||
Section "Uninstall"
|
|
||||||
!insertmacro customUnInstall
|
|
||||||
|
|
||||||
Delete "$INSTDIR\Uninstall.exe"
|
|
||||||
RMDir /r "$INSTDIR"
|
|
||||||
|
|
||||||
DeleteRegKey HKCU "Software\Tom Butcher\Farm Control"
|
|
||||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control"
|
|
||||||
SectionEnd
|
|
||||||
@ -1,460 +0,0 @@
|
|||||||
!include "FileFunc.nsh"
|
|
||||||
!insertmacro GetParameters
|
|
||||||
!insertmacro GetOptions
|
|
||||||
|
|
||||||
Var ProgressLogFile
|
|
||||||
Var IsInAppUpdate
|
|
||||||
Var RestartAfterInstall
|
|
||||||
Var FinalInstDir
|
|
||||||
Var UpdateParentPid
|
|
||||||
|
|
||||||
; Generate an include at compile time that defines APP_COPY_TOTAL_BYTES and
|
|
||||||
; copyApplicationFilesWithProgress. The generated macro contains one File
|
|
||||||
; command and one byte-progress log entry for every file in the source tree.
|
|
||||||
!macro compileProgressFileCopy sourceDir generator
|
|
||||||
!tempfile GENERATED_FILE_COPY_INCLUDE
|
|
||||||
!system 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${generator}" -SourceDir "${sourceDir}" -OutputPath "${GENERATED_FILE_COPY_INCLUDE}"' = 0
|
|
||||||
!include "${GENERATED_FILE_COPY_INCLUDE}"
|
|
||||||
!delfile "${GENERATED_FILE_COPY_INCLUDE}"
|
|
||||||
!undef GENERATED_FILE_COPY_INCLUDE
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro initProgressLog
|
|
||||||
StrCpy $ProgressLogFile ""
|
|
||||||
StrCpy $IsInAppUpdate "0"
|
|
||||||
StrCpy $RestartAfterInstall "0"
|
|
||||||
StrCpy $FinalInstDir ""
|
|
||||||
StrCpy $UpdateParentPid "0"
|
|
||||||
|
|
||||||
${GetParameters} $R9
|
|
||||||
|
|
||||||
ClearErrors
|
|
||||||
${GetOptions} $R9 "/UPDATE" $R8
|
|
||||||
${IfNot} ${Errors}
|
|
||||||
StrCpy $IsInAppUpdate "1"
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
ClearErrors
|
|
||||||
${GetOptions} $R9 "/RESTARTFC" $R8
|
|
||||||
${IfNot} ${Errors}
|
|
||||||
StrCpy $RestartAfterInstall "1"
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
ClearErrors
|
|
||||||
${GetOptions} $R9 "/PARENTPID=" $R8
|
|
||||||
${IfNot} ${Errors}
|
|
||||||
; Convert to an integer before it is used in Win32 calls or taskkill.
|
|
||||||
IntOp $UpdateParentPid $R8 + 0
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
; Prefer env var so paths with spaces are reliable; /LOG= remains supported.
|
|
||||||
ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG"
|
|
||||||
${If} $ProgressLogFile == ""
|
|
||||||
ClearErrors
|
|
||||||
${GetOptions} $R9 "/LOG=" $ProgressLogFile
|
|
||||||
${If} ${Errors}
|
|
||||||
StrCpy $ProgressLogFile ""
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
${If} $ProgressLogFile != ""
|
|
||||||
; Strip surrounding quotes from /LOG="C:\path with spaces\log.log"
|
|
||||||
StrCpy $R8 $ProgressLogFile 1
|
|
||||||
${If} $R8 == '"'
|
|
||||||
StrCpy $ProgressLogFile $ProgressLogFile "" 1
|
|
||||||
StrLen $R8 $ProgressLogFile
|
|
||||||
IntOp $R8 $R8 - 1
|
|
||||||
${If} $R8 > 0
|
|
||||||
StrCpy $ProgressLogFile $ProgressLogFile $R8
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
; Truncate any previous log so progress starts clean.
|
|
||||||
Push $0
|
|
||||||
FileOpen $0 "$ProgressLogFile" w
|
|
||||||
${If} $0 != ""
|
|
||||||
FileClose $0
|
|
||||||
${EndIf}
|
|
||||||
Pop $0
|
|
||||||
${EndIf}
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
; Open/append/close each line so the updater can read the log concurrently
|
|
||||||
; (avoids an exclusive lock for the whole install; LogEx SHARE_READ is an alternative).
|
|
||||||
!macro progressLog line
|
|
||||||
DetailPrint "${line}"
|
|
||||||
${If} $ProgressLogFile != ""
|
|
||||||
Push $0
|
|
||||||
FileOpen $0 "$ProgressLogFile" a
|
|
||||||
${If} $0 != ""
|
|
||||||
FileSeek $0 0 END
|
|
||||||
FileWrite $0 "${line}$\r$\n"
|
|
||||||
FileClose $0
|
|
||||||
${EndIf}
|
|
||||||
Pop $0
|
|
||||||
${EndIf}
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!define PROGRESS_START 1
|
|
||||||
!define PREPARE_PROGRESS 3
|
|
||||||
!define COPY_PROGRESS_START 5
|
|
||||||
!define COPY_PROGRESS_END 90
|
|
||||||
!define CONFIGURE_PROGRESS 91
|
|
||||||
!define SHORTCUTS_PROGRESS 93
|
|
||||||
!define FINALIZE_PROGRESS 95
|
|
||||||
!define UNINSTALLER_PROGRESS 97
|
|
||||||
!define COMPLETE_PROGRESS 99
|
|
||||||
|
|
||||||
!macro progressPercent percent
|
|
||||||
!insertmacro progressLog "installer:%${percent}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressStart
|
|
||||||
!insertmacro progressPercent "${PROGRESS_START}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressPrepare
|
|
||||||
!insertmacro progressPercent "${PREPARE_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressCopyStart
|
|
||||||
!insertmacro progressPercent "${COPY_PROGRESS_START}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressCopyComplete
|
|
||||||
!insertmacro progressPercent "${COPY_PROGRESS_END}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressConfigure
|
|
||||||
!insertmacro progressPercent "${CONFIGURE_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressShortcuts
|
|
||||||
!insertmacro progressPercent "${SHORTCUTS_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressFinalizeInstall
|
|
||||||
!insertmacro progressPercent "${FINALIZE_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressWriteUninstaller
|
|
||||||
!insertmacro progressPercent "${UNINSTALLER_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressFinalize
|
|
||||||
!insertmacro progressPercent "${COMPLETE_PROGRESS}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressPhase phase
|
|
||||||
!insertmacro progressLog "installer:PHASE:${phase}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressStatus status
|
|
||||||
!insertmacro progressLog "installer:STATUS:${status}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressCopyTotal bytes
|
|
||||||
!insertmacro progressLog "installer:COPY_TOTAL:${bytes}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressCopyFile bytes relativePath
|
|
||||||
!insertmacro progressLog "installer:COPY_FILE:${bytes}:${relativePath}"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressSuccess
|
|
||||||
!insertmacro progressPercent 100
|
|
||||||
!insertmacro progressStatus "Installation complete. Restarting Farm Control..."
|
|
||||||
!insertmacro progressLog "installer: The install was successful."
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro progressFailure message
|
|
||||||
!insertmacro progressLog "installer:STATUS:${message}"
|
|
||||||
!insertmacro progressLog "installer: The install failed."
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro quitFarmControl
|
|
||||||
!insertmacro progressPhase "Stopping Farm Control"
|
|
||||||
!insertmacro progressStatus "Stopping running Farm Control processes..."
|
|
||||||
DetailPrint "Stopping running Farm Control processes..."
|
|
||||||
ExecWait 'taskkill /F /IM FarmControl.exe /T' $R0
|
|
||||||
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
; Side-by-side update: install into Farm Control.new while the running app
|
|
||||||
; keeps using Farm Control, then swap folders after the process exits.
|
|
||||||
!macro prepareSideBySideUpdate
|
|
||||||
!insertmacro progressPhase "Preparing update"
|
|
||||||
!insertmacro progressStatus "Preparing staging folder for update..."
|
|
||||||
|
|
||||||
StrCpy $FinalInstDir $INSTDIR
|
|
||||||
StrCpy $INSTDIR "$FinalInstDir.new"
|
|
||||||
|
|
||||||
${If} ${FileExists} "$INSTDIR"
|
|
||||||
DetailPrint "Removing leftover staging folder..."
|
|
||||||
RMDir /r "$INSTDIR"
|
|
||||||
${EndIf}
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
; Swap order: Farm Control -> .old, Farm Control.new -> Farm Control, delete .old.
|
|
||||||
!macro swapUpdateStaging
|
|
||||||
!insertmacro progressPhase "Applying update"
|
|
||||||
!insertmacro progressStatus "Replacing Farm Control with the new version..."
|
|
||||||
DetailPrint "Replacing Farm Control with staged update..."
|
|
||||||
|
|
||||||
; The install section ran SetOutPath into the install tree; a directory that
|
|
||||||
; is any process's working directory cannot be renamed, so move out first.
|
|
||||||
SetOutPath "$TEMP"
|
|
||||||
|
|
||||||
Sleep 3000
|
|
||||||
|
|
||||||
; Clear a leftover .old from a previous interrupted update.
|
|
||||||
${If} ${FileExists} "$FinalInstDir.old"
|
|
||||||
DetailPrint "Removing leftover Farm Control.old..."
|
|
||||||
RMDir /r "$FinalInstDir.old"
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
${If} ${FileExists} "$FinalInstDir"
|
|
||||||
!insertmacro progressStatus "Waiting for Farm Control to release install files..."
|
|
||||||
DetailPrint "Waiting to rename install directory to Farm Control.old..."
|
|
||||||
|
|
||||||
; Retry every 500ms; success is verified on disk rather than via the
|
|
||||||
; error flag, which Rename does not set reliably.
|
|
||||||
StrCpy $R7 0
|
|
||||||
swap_rename_old_retry:
|
|
||||||
Rename "$FinalInstDir" "$FinalInstDir.old"
|
|
||||||
${IfNot} ${FileExists} "$FinalInstDir"
|
|
||||||
Goto swap_rename_old_done
|
|
||||||
${EndIf}
|
|
||||||
IntOp $R7 $R7 + 1
|
|
||||||
${If} $R7 < 120
|
|
||||||
Sleep 500
|
|
||||||
Goto swap_rename_old_retry
|
|
||||||
${EndIf}
|
|
||||||
!insertmacro progressFailure "Could not move the previous Farm Control installation aside."
|
|
||||||
Abort
|
|
||||||
swap_rename_old_done:
|
|
||||||
DetailPrint "Install directory renamed to Farm Control.old"
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro progressStatus "Activating new Farm Control installation..."
|
|
||||||
DetailPrint "Renaming Farm Control.new to Farm Control..."
|
|
||||||
|
|
||||||
StrCpy $R7 0
|
|
||||||
swap_rename_new_retry:
|
|
||||||
Rename "$INSTDIR" "$FinalInstDir"
|
|
||||||
${If} ${FileExists} "$FinalInstDir"
|
|
||||||
Goto swap_rename_new_done
|
|
||||||
${EndIf}
|
|
||||||
IntOp $R7 $R7 + 1
|
|
||||||
${If} $R7 < 20
|
|
||||||
Sleep 500
|
|
||||||
Goto swap_rename_new_retry
|
|
||||||
${EndIf}
|
|
||||||
; Best-effort rollback so the previous install is usable again.
|
|
||||||
${If} ${FileExists} "$FinalInstDir.old"
|
|
||||||
Rename "$FinalInstDir.old" "$FinalInstDir"
|
|
||||||
${EndIf}
|
|
||||||
!insertmacro progressFailure "Could not activate the new Farm Control installation."
|
|
||||||
Abort
|
|
||||||
swap_rename_new_done:
|
|
||||||
|
|
||||||
StrCpy $INSTDIR $FinalInstDir
|
|
||||||
!insertmacro progressLog "installer:STATUS:Update files activated"
|
|
||||||
|
|
||||||
${If} ${FileExists} "$FinalInstDir.old"
|
|
||||||
!insertmacro progressStatus "Removing previous Farm Control installation..."
|
|
||||||
DetailPrint "Removing Farm Control.old..."
|
|
||||||
|
|
||||||
StrCpy $R7 0
|
|
||||||
swap_delete_old_retry:
|
|
||||||
RMDir /r "$FinalInstDir.old"
|
|
||||||
${If} ${FileExists} "$FinalInstDir.old"
|
|
||||||
IntOp $R7 $R7 + 1
|
|
||||||
${If} $R7 < 20
|
|
||||||
Sleep 500
|
|
||||||
Goto swap_delete_old_retry
|
|
||||||
${EndIf}
|
|
||||||
; New version is already active; leftover .old is non-fatal.
|
|
||||||
DetailPrint "Warning: could not fully remove Farm Control.old"
|
|
||||||
!insertmacro progressLog "installer:STATUS:Warning: could not fully remove previous installation"
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro uninstallPreviousFarmControl
|
|
||||||
ClearErrors
|
|
||||||
ReadRegStr $R0 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString"
|
|
||||||
StrCmp $R0 "" check_install_dir run_uninstall
|
|
||||||
|
|
||||||
check_install_dir:
|
|
||||||
ReadRegStr $R0 HKCU "Software\Tom Butcher\Farm Control" "InstallDir"
|
|
||||||
StrCmp $R0 "" check_legacy_uninstall
|
|
||||||
StrCpy $R0 "$R0\Uninstall.exe"
|
|
||||||
Goto run_uninstall
|
|
||||||
|
|
||||||
check_legacy_uninstall:
|
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString"
|
|
||||||
StrCmp $R0 "" check_legacy_install_dir run_uninstall
|
|
||||||
|
|
||||||
check_legacy_install_dir:
|
|
||||||
ReadRegStr $R0 HKLM "Software\Tom Butcher\Farm Control" "InstallDir"
|
|
||||||
StrCmp $R0 "" check_default_dir
|
|
||||||
StrCpy $R0 "$R0\Uninstall.exe"
|
|
||||||
Goto run_uninstall
|
|
||||||
|
|
||||||
check_default_dir:
|
|
||||||
StrCpy $R0 "$LOCALAPPDATA\Programs\Farm Control\Uninstall.exe"
|
|
||||||
IfFileExists $R0 run_uninstall check_legacy_default_dir
|
|
||||||
|
|
||||||
check_legacy_default_dir:
|
|
||||||
StrCpy $R0 "$PROGRAMFILES64\Farm Control\Uninstall.exe"
|
|
||||||
IfFileExists $R0 run_uninstall done_uninstall
|
|
||||||
|
|
||||||
run_uninstall:
|
|
||||||
IfFileExists $R0 0 done_uninstall
|
|
||||||
!insertmacro progressPhase "Removing previous version"
|
|
||||||
!insertmacro progressStatus "Removing previous Farm Control installation..."
|
|
||||||
DetailPrint "Removing previous Farm Control installation..."
|
|
||||||
!insertmacro quitFarmControl
|
|
||||||
ExecWait '"$R0" /S' $R1
|
|
||||||
DetailPrint "Previous installation removed (exit code: $R1)"
|
|
||||||
!insertmacro progressLog "installer:STATUS:Previous installation removed"
|
|
||||||
|
|
||||||
done_uninstall:
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro createDesktopShortcut
|
|
||||||
SetShellVarContext current
|
|
||||||
SetOutPath "$FinalInstDir\bin"
|
|
||||||
CreateShortCut "$DESKTOP\Farm Control.lnk" "$FinalInstDir\bin\FarmControl.exe" "" "$FinalInstDir\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro createStartMenuShortcut
|
|
||||||
SetShellVarContext current
|
|
||||||
CreateDirectory "$SMPROGRAMS\Farm Control"
|
|
||||||
SetOutPath "$FinalInstDir\bin"
|
|
||||||
CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$FinalInstDir\bin\FarmControl.exe" "" "$FinalInstDir\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro removeDesktopShortcut
|
|
||||||
SetShellVarContext current
|
|
||||||
Delete "$DESKTOP\Farm Control.lnk"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro removeStartMenuShortcut
|
|
||||||
SetShellVarContext current
|
|
||||||
Delete "$SMPROGRAMS\Farm Control\Farm Control.lnk"
|
|
||||||
RMDir "$SMPROGRAMS\Farm Control"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro customInstall
|
|
||||||
!insertmacro progressConfigure
|
|
||||||
!insertmacro progressPhase "Configuring Farm Control"
|
|
||||||
!insertmacro progressStatus "Registering farmcontrol URI handler..."
|
|
||||||
DetailPrint "Register farmcontrol URI Handler"
|
|
||||||
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol"
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol" "URL Protocol" ""
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\DefaultIcon" "" "$FinalInstDir\bin\FarmControl.exe"
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell" "" ""
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" ""
|
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" '"$FinalInstDir\bin\bun.exe" "$FinalInstDir\bin\deeplink.js" "%1"'
|
|
||||||
|
|
||||||
!insertmacro progressShortcuts
|
|
||||||
!insertmacro progressStatus "Creating shortcuts..."
|
|
||||||
DetailPrint "Creating shortcuts"
|
|
||||||
!insertmacro createDesktopShortcut
|
|
||||||
!insertmacro createStartMenuShortcut
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
!macro customUnInstall
|
|
||||||
!insertmacro removeDesktopShortcut
|
|
||||||
!insertmacro removeStartMenuShortcut
|
|
||||||
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
|
||||||
!macroend
|
|
||||||
|
|
||||||
; Wait for the running app to exit, swap staged update into place, then relaunch.
|
|
||||||
; Only used when /RESTARTFC is passed.
|
|
||||||
Function restartFarmControlAfterUpdate
|
|
||||||
${If} $RestartAfterInstall != "1"
|
|
||||||
Return
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
; All files are copied and we are about to rename the install directory.
|
|
||||||
; Emit the success line (matched by isWindowsInstallSuccessful in
|
|
||||||
; winappupdate.js) so the app knows to quit and restart; this installer
|
|
||||||
; stays alive to swap the folders and relaunch Farm Control.
|
|
||||||
!insertmacro progressSuccess
|
|
||||||
|
|
||||||
!insertmacro progressStatus "Waiting for Farm Control to close..."
|
|
||||||
|
|
||||||
${If} $UpdateParentPid > 0
|
|
||||||
; Wait for the exact updater process rather than an executable name. CEF
|
|
||||||
; builds can keep bun/renderer processes alive after the launcher exits.
|
|
||||||
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $UpdateParentPid) p .r0'
|
|
||||||
${If} $0 != 0
|
|
||||||
; Allow two seconds for a graceful exit, then terminate the process.
|
|
||||||
System::Call 'kernel32::WaitForSingleObject(p r0, i 2000) i .r1'
|
|
||||||
${If} $1 == 258
|
|
||||||
!insertmacro progressStatus "Stopping Farm Control..."
|
|
||||||
DetailPrint "Farm Control process $UpdateParentPid did not exit; terminating it..."
|
|
||||||
; Never use taskkill /T here: this installer is a direct child of the
|
|
||||||
; updater process, so a tree kill would terminate the installer itself
|
|
||||||
; before the folder swap (CEF builds hung at "Stopping Farm Control...").
|
|
||||||
ExecWait 'taskkill /F /PID $UpdateParentPid' $R0
|
|
||||||
|
|
||||||
; Verify that taskkill actually terminated the process before swapping.
|
|
||||||
System::Call 'kernel32::WaitForSingleObject(p r0, i 10000) i .r1'
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
System::Call 'kernel32::CloseHandle(p r0)'
|
|
||||||
${If} $1 != 0
|
|
||||||
!insertmacro progressFailure "Could not stop the running Farm Control process."
|
|
||||||
Abort
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
${Else}
|
|
||||||
; Compatibility fallback for manually launched older installers.
|
|
||||||
Sleep 2000
|
|
||||||
!insertmacro quitFarmControl
|
|
||||||
|
|
||||||
; Confirm both known launcher names have stopped before continuing.
|
|
||||||
StrCpy $R7 0
|
|
||||||
restart_verify_loop:
|
|
||||||
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0
|
|
||||||
${If} $R0 != 0
|
|
||||||
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0
|
|
||||||
${EndIf}
|
|
||||||
${If} $R0 != 0
|
|
||||||
Goto restart_wait_done
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
IntOp $R7 $R7 + 1
|
|
||||||
${If} $R7 < 20
|
|
||||||
Sleep 500
|
|
||||||
Goto restart_verify_loop
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro progressFailure "Could not stop the running Farm Control process."
|
|
||||||
Abort
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
restart_wait_done:
|
|
||||||
; CEF helper processes normally exit on their own once the main process is
|
|
||||||
; gone, but sweep up any stragglers still running from the install
|
|
||||||
; directory so the rename cannot be blocked. Matching on executable path
|
|
||||||
; (not a tree kill) guarantees this installer, which runs from the Updates
|
|
||||||
; folder, is never terminated.
|
|
||||||
ExecWait `powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Where-Object { $$_.ExecutablePath -like '$FinalInstDir\*' } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"` $R0
|
|
||||||
|
|
||||||
; Give CEF descendants a moment to release DLLs before renaming the tree.
|
|
||||||
Sleep 3000
|
|
||||||
|
|
||||||
${If} $IsInAppUpdate == "1"
|
|
||||||
!insertmacro swapUpdateStaging
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro progressStatus "Starting Farm Control..."
|
|
||||||
SetOutPath "$FinalInstDir\bin"
|
|
||||||
Exec "$FinalInstDir\bin\FarmControl.exe"
|
|
||||||
FunctionEnd
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
|
||||||
<!--
|
|
||||||
MSI wrapper around the compiled NSIS setup.exe.
|
|
||||||
Runs the bundled installer with the same silent in-app update flags used by
|
|
||||||
src/desktop/winappupdate.js: /S /UPDATE /RESTARTFC /LOG=...
|
|
||||||
|
|
||||||
ALLUSERS=2 + MSIINSTALLPERUSER=1 defaults to per-user (matching NSIS) and is
|
|
||||||
not blocked by DisableMsi=1 / error 1625 the way a pure InstallScope=perUser
|
|
||||||
package is.
|
|
||||||
-->
|
|
||||||
<Product
|
|
||||||
Id="*"
|
|
||||||
Name="Farm Control"
|
|
||||||
Language="1033"
|
|
||||||
Version="__VERSION__"
|
|
||||||
Manufacturer="Tom Butcher"
|
|
||||||
UpgradeCode="__UPGRADE_CODE__">
|
|
||||||
<Package
|
|
||||||
InstallerVersion="500"
|
|
||||||
Compressed="yes"
|
|
||||||
InstallPrivileges="limited"
|
|
||||||
Platform="x64" />
|
|
||||||
|
|
||||||
<Condition Message="Windows 7 and above is required"><![CDATA[Installed OR VersionNT >= 601]]></Condition>
|
|
||||||
|
|
||||||
<MajorUpgrade
|
|
||||||
AllowSameVersionUpgrades="yes"
|
|
||||||
DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
|
||||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
|
||||||
|
|
||||||
<Icon Id="InstallerIcon" SourceFile="__INSTALLER_ICON__" />
|
|
||||||
<Property Id="ARPPRODUCTICON" Value="InstallerIcon" />
|
|
||||||
|
|
||||||
<Property Id="DISABLEADVTSHORTCUTS" Value="1" />
|
|
||||||
<Property Id="ALLUSERS" Secure="yes" Value="2" />
|
|
||||||
<Property Id="MSIINSTALLPERUSER" Secure="yes" Value="1" />
|
|
||||||
<Property Id="REBOOT" Value="ReallySuppress" />
|
|
||||||
|
|
||||||
<Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" />
|
|
||||||
|
|
||||||
<!-- ExeCommand is a Formatted field, so [FarmControlUpdatesDir] resolves to the
|
|
||||||
real per-user path here. An intermediate Property would not resolve it
|
|
||||||
(property values are not recursively formatted). -->
|
|
||||||
<CustomAction
|
|
||||||
Id="RunInstaller"
|
|
||||||
BinaryKey="WrappedExe"
|
|
||||||
ExeCommand="/S /UPDATE /RESTARTFC /LOG="[FarmControlUpdatesDir]install.log""
|
|
||||||
Execute="immediate"
|
|
||||||
Impersonate="yes"
|
|
||||||
Return="check" />
|
|
||||||
|
|
||||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
|
||||||
<Directory Id="LocalAppDataFolder">
|
|
||||||
<Directory Id="FarmControlAppDataDir" Name="FarmControl">
|
|
||||||
<Directory Id="FarmControlUpdatesDir" Name="Updates">
|
|
||||||
<Component Id="UpdatesFolderComponent" Guid="A1B2C3D4-E5F6-7890-ABCD-EF1234567890">
|
|
||||||
<CreateFolder />
|
|
||||||
<!-- ICE64: per-user directories must be scheduled for removal on uninstall. -->
|
|
||||||
<RemoveFile Id="RemoveUpdatesFiles" Directory="FarmControlUpdatesDir" Name="*" On="uninstall" />
|
|
||||||
<RemoveFolder Id="RemoveFarmControlUpdatesDir" Directory="FarmControlUpdatesDir" On="uninstall" />
|
|
||||||
<RemoveFolder Id="RemoveFarmControlAppDataDir" Directory="FarmControlAppDataDir" On="uninstall" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCU"
|
|
||||||
Key="Software\Tom Butcher\Farm Control"
|
|
||||||
Name="UpdatesDir"
|
|
||||||
Type="string"
|
|
||||||
Value="[FarmControlUpdatesDir]"
|
|
||||||
KeyPath="yes" />
|
|
||||||
</Component>
|
|
||||||
</Directory>
|
|
||||||
</Directory>
|
|
||||||
</Directory>
|
|
||||||
<Component Id="PerUserMarker" Guid="8A3F2E1D-9C4B-4A7E-B6D5-1F0E3C2B4A59">
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCU"
|
|
||||||
Key="Software\Tom Butcher\Farm Control"
|
|
||||||
Name="MsiWrapper"
|
|
||||||
Type="integer"
|
|
||||||
Value="1"
|
|
||||||
KeyPath="yes" />
|
|
||||||
</Component>
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Feature Id="MainFeature" Title="Farm Control" Level="1">
|
|
||||||
<ComponentRef Id="UpdatesFolderComponent" />
|
|
||||||
<ComponentRef Id="PerUserMarker" />
|
|
||||||
</Feature>
|
|
||||||
|
|
||||||
<InstallExecuteSequence>
|
|
||||||
<Custom Action="RunInstaller" After="InstallFiles">NOT Installed</Custom>
|
|
||||||
</InstallExecuteSequence>
|
|
||||||
</Product>
|
|
||||||
</Wix>
|
|
||||||
2527
pnpm-lock.yaml
generated
@ -2,15 +2,8 @@ allowBuilds:
|
|||||||
electron: true
|
electron: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
sharp: true
|
sharp: true
|
||||||
|
keytar: true
|
||||||
workerd: true
|
workerd: true
|
||||||
core-js: true
|
core-js: true
|
||||||
electron-winstaller: true
|
electron-winstaller: true
|
||||||
'@tsparticles/engine': true
|
'@tsparticles/engine': true
|
||||||
|
|
||||||
overrides:
|
|
||||||
baseline-browser-mapping: 2.11.4
|
|
||||||
'@codemirror/state': 6.7.1
|
|
||||||
'@codemirror/view': 6.39.12
|
|
||||||
'@codemirror/language': 6.12.1
|
|
||||||
'@codemirror/commands': 6.10.4
|
|
||||||
'@codemirror/autocomplete': 6.20.0
|
|
||||||
@ -1,260 +0,0 @@
|
|||||||
import { createWriteStream, promises as fs } from "node:fs";
|
|
||||||
import http from "node:http";
|
|
||||||
import https from "node:https";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import process from "node:process";
|
|
||||||
import { Utils } from "electrobun/bun";
|
|
||||||
import { launchMacInstaller } from "./macappupdate.js";
|
|
||||||
import { launchWindowsInstaller } from "./winappupdate.js";
|
|
||||||
import { scheduleAppRestart } from "./updater-runner.js";
|
|
||||||
|
|
||||||
const SUPPORTED_TARGETS = {
|
|
||||||
darwin: {
|
|
||||||
extension: ".pkg",
|
|
||||||
osMatchers: ["darwin", "mac", "macos", "osx"],
|
|
||||||
},
|
|
||||||
win32: {
|
|
||||||
extension: ".msi",
|
|
||||||
osMatchers: ["win32", "win", "windows"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let runningUpdate = null;
|
|
||||||
|
|
||||||
const getArtifactName = (artifact) =>
|
|
||||||
String(artifact?.fileName || artifact?.relativePath || artifact?.url || "");
|
|
||||||
|
|
||||||
const normalizeArch = (arch) => {
|
|
||||||
if (arch === "x64" || arch === "amd64") return "x64";
|
|
||||||
if (arch === "arm64" || arch === "aarch64") return "arm64";
|
|
||||||
return arch;
|
|
||||||
};
|
|
||||||
|
|
||||||
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
|
|
||||||
const name = getArtifactName(artifact).toLowerCase();
|
|
||||||
const normalizedArch = normalizeArch(arch);
|
|
||||||
const artifactArch = normalizeArch(String(artifact?.arch || "").toLowerCase());
|
|
||||||
const artifactPlatform = String(
|
|
||||||
artifact?.platform || artifact?.os || artifact?.target || "",
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (!name.endsWith(target.extension)) return false;
|
|
||||||
if (!artifact?.url) return false;
|
|
||||||
|
|
||||||
const matchesArch =
|
|
||||||
artifactArch === normalizedArch ||
|
|
||||||
name.includes(`-${normalizedArch}`) ||
|
|
||||||
name.includes(`_${normalizedArch}`) ||
|
|
||||||
name.includes(`.${normalizedArch}.`) ||
|
|
||||||
name.includes(normalizedArch);
|
|
||||||
|
|
||||||
const matchesOs =
|
|
||||||
!artifactPlatform ||
|
|
||||||
target.osMatchers.includes(artifactPlatform) ||
|
|
||||||
target.osMatchers.some((matcher) => name.includes(matcher)) ||
|
|
||||||
(platform === "darwin" && name.includes("mac")) ||
|
|
||||||
(platform === "win32" && name.includes("win"));
|
|
||||||
|
|
||||||
return matchesArch && matchesOs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectUpdateArtifact = (
|
|
||||||
update,
|
|
||||||
platform = process.platform,
|
|
||||||
arch = process.arch,
|
|
||||||
) => {
|
|
||||||
const target = SUPPORTED_TARGETS[platform];
|
|
||||||
if (!target) {
|
|
||||||
throw new Error(`App updates are not supported on ${platform}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : [];
|
|
||||||
const matchingArtifact = artifacts.find((artifact) =>
|
|
||||||
artifactMatchesPlatform(artifact, target, platform, arch),
|
|
||||||
);
|
|
||||||
const fallbackArtifact = artifacts.find((artifact) => {
|
|
||||||
const name = getArtifactName(artifact).toLowerCase();
|
|
||||||
return artifact?.url && name.endsWith(target.extension);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchingArtifact && !fallbackArtifact) {
|
|
||||||
throw new Error(
|
|
||||||
`No ${target.extension} update artifact found for ${platform}/${arch}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchingArtifact || fallbackArtifact;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInstallErrorMessage = (error, output = "") => {
|
|
||||||
const combined = `${output}\n${error?.message || ""}`.trim();
|
|
||||||
|
|
||||||
if (
|
|
||||||
/cancel/i.test(combined) ||
|
|
||||||
/did not grant permission/i.test(combined) ||
|
|
||||||
/user canceled/i.test(combined)
|
|
||||||
) {
|
|
||||||
return "Update installation was cancelled.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/incorrect/i.test(combined)) {
|
|
||||||
return "The administrator password was incorrect.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
/1625/.test(combined) ||
|
|
||||||
/forbidden by system policy/i.test(combined) ||
|
|
||||||
/Non-assigned apps are disabled/i.test(combined)
|
|
||||||
) {
|
|
||||||
return "Update installation was blocked by Windows Installer policy.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined || "Failed to install update.";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDownloadUrl = (url, redirectCount = 0) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
if (redirectCount > 5) {
|
|
||||||
reject(new Error("Too many redirects while downloading update."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
const client = parsedUrl.protocol === "https:" ? https : http;
|
|
||||||
const request = client.get(parsedUrl, (response) => {
|
|
||||||
const location = response.headers.location;
|
|
||||||
|
|
||||||
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
|
|
||||||
response.resume();
|
|
||||||
resolve(
|
|
||||||
getDownloadUrl(
|
|
||||||
new URL(location, parsedUrl).toString(),
|
|
||||||
redirectCount + 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({ response, url: parsedUrl.toString() });
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
|
|
||||||
const { response } = await getDownloadUrl(artifact.url);
|
|
||||||
|
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
||||||
response.resume();
|
|
||||||
throw new Error(`Update download failed with HTTP ${response.statusCode}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalBytes =
|
|
||||||
Number.parseInt(response.headers["content-length"], 10) || 0;
|
|
||||||
let downloadedBytes = 0;
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const output = createWriteStream(destinationPath);
|
|
||||||
|
|
||||||
response.on("data", (chunk) => {
|
|
||||||
downloadedBytes += chunk.length;
|
|
||||||
const percent = totalBytes
|
|
||||||
? Math.round((downloadedBytes / totalBytes) * 100)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
sendProgress({
|
|
||||||
phase: "downloading",
|
|
||||||
percent,
|
|
||||||
downloadedBytes,
|
|
||||||
totalBytes,
|
|
||||||
message: totalBytes
|
|
||||||
? `Downloading update (${percent}%)`
|
|
||||||
: "Downloading update",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
response.on("error", reject);
|
|
||||||
output.on("error", reject);
|
|
||||||
output.on("finish", resolve);
|
|
||||||
response.pipe(output);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const launchInstallerAndRestart = async (
|
|
||||||
mainWindow,
|
|
||||||
installerPath,
|
|
||||||
sendProgress,
|
|
||||||
) => {
|
|
||||||
const installerHelpers = { sendProgress, getInstallErrorMessage };
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
await launchMacInstaller(
|
|
||||||
mainWindow,
|
|
||||||
installerPath,
|
|
||||||
sendProgress,
|
|
||||||
installerHelpers,
|
|
||||||
);
|
|
||||||
} else if (process.platform === "win32") {
|
|
||||||
await launchWindowsInstaller(
|
|
||||||
mainWindow,
|
|
||||||
installerPath,
|
|
||||||
sendProgress,
|
|
||||||
installerHelpers,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error(`App updates are not supported on ${process.platform}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleAppRestart();
|
|
||||||
Utils.quit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
|
||||||
const artifact = selectUpdateArtifact(update);
|
|
||||||
const tempDirectory = await fs.mkdtemp(
|
|
||||||
path.join(os.tmpdir(), "farmcontrol-update-"),
|
|
||||||
);
|
|
||||||
const artifactName = path.basename(getArtifactName(artifact));
|
|
||||||
const installerPath = path.join(tempDirectory, artifactName);
|
|
||||||
|
|
||||||
sendProgress({
|
|
||||||
phase: "preparing",
|
|
||||||
percent: 0,
|
|
||||||
artifact,
|
|
||||||
message: "Preparing update download",
|
|
||||||
});
|
|
||||||
|
|
||||||
await downloadArtifact(artifact, installerPath, sendProgress);
|
|
||||||
|
|
||||||
sendProgress({
|
|
||||||
phase: "downloaded",
|
|
||||||
percent: 100,
|
|
||||||
downloadedBytes: null,
|
|
||||||
totalBytes: null,
|
|
||||||
artifact,
|
|
||||||
message: "Update downloaded",
|
|
||||||
});
|
|
||||||
|
|
||||||
await launchInstallerAndRestart(mainWindow, installerPath, sendProgress);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function startAppUpdate(mainWindow, update, sendProgress) {
|
|
||||||
if (runningUpdate) return runningUpdate;
|
|
||||||
|
|
||||||
runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
|
|
||||||
.then(() => ({ ok: true }))
|
|
||||||
.catch((error) => {
|
|
||||||
sendProgress({
|
|
||||||
phase: "error",
|
|
||||||
percent: null,
|
|
||||||
message: error?.message || "Failed to update app.",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
runningUpdate = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return runningUpdate;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import { app, ipcMain, shell, globalShortcut, safeStorage } from 'electron'
|
import { app, ipcMain, shell, globalShortcut } from 'electron'
|
||||||
import Store from 'electron-store'
|
import { createRequire } from 'module'
|
||||||
import { Buffer } from 'buffer'
|
|
||||||
import process from 'process'
|
|
||||||
import {
|
import {
|
||||||
registerGlobalShortcuts,
|
registerGlobalShortcuts,
|
||||||
setupSpotlightIPC
|
setupSpotlightIPC
|
||||||
@ -14,66 +12,22 @@ import {
|
|||||||
setupSingleInstanceLock,
|
setupSingleInstanceLock,
|
||||||
handleDeepLinkFromArgv
|
handleDeepLinkFromArgv
|
||||||
} from './mainWindow.js'
|
} from './mainWindow.js'
|
||||||
import { setupAppUpdateIPC } from './appupdate.js'
|
|
||||||
|
|
||||||
// --- Auth session storage (main process) ---
|
// --- Keytar-backed auth session storage (main process) ---
|
||||||
const authStore = new Store({
|
const require = createRequire(import.meta.url)
|
||||||
name: 'auth-session'
|
let keytar = null
|
||||||
})
|
try {
|
||||||
const AUTH_SESSION_KEY = 'authSession'
|
// keytar is a native module; in some dev environments it may not be built yet.
|
||||||
const appSettingsStore = new Store({
|
keytar = require('keytar')
|
||||||
name: 'settings'
|
} catch (e) {
|
||||||
})
|
|
||||||
const APP_SETTINGS_KEY = 'appSettings'
|
|
||||||
|
|
||||||
const serializeAuthSession = (session) => {
|
|
||||||
const sessionJson = JSON.stringify(session)
|
|
||||||
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
|
||||||
const encrypted = safeStorage.encryptString(sessionJson).toString('base64')
|
|
||||||
return {
|
|
||||||
encrypted: true,
|
|
||||||
value: encrypted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
encrypted: false,
|
|
||||||
value: sessionJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deserializeAuthSession = (storedValue) => {
|
|
||||||
if (!storedValue) return null
|
|
||||||
|
|
||||||
if (typeof storedValue === 'object' && storedValue.encrypted === true) {
|
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
'[auth-session] Encrypted auth session exists but encryption is unavailable on this system.'
|
'[keytar] Not available; auth session persistence will be disabled.',
|
||||||
|
e?.message || e
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
}
|
|
||||||
const decrypted = safeStorage.decryptString(
|
|
||||||
Buffer.from(storedValue.value, 'base64')
|
|
||||||
)
|
|
||||||
return JSON.parse(decrypted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof storedValue === 'object' && typeof storedValue.value === 'string') {
|
const KEYTAR_SERVICE = app.name || 'Farm Control'
|
||||||
return JSON.parse(storedValue.value)
|
const KEYTAR_ACCOUNT = 'authSession'
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof storedValue === 'string') {
|
|
||||||
return JSON.parse(storedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy safety net if the object shape already matches the session structure.
|
|
||||||
if (typeof storedValue === 'object' && storedValue.token) {
|
|
||||||
return storedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const gotTheLock = setupSingleInstanceLock(app)
|
const gotTheLock = setupSingleInstanceLock(app)
|
||||||
|
|
||||||
@ -83,7 +37,6 @@ if (gotTheLock) {
|
|||||||
registerGlobalShortcuts()
|
registerGlobalShortcuts()
|
||||||
setupSpotlightIPC()
|
setupSpotlightIPC()
|
||||||
setupMainWindowIPC()
|
setupMainWindowIPC()
|
||||||
setupAppUpdateIPC(app)
|
|
||||||
setupMainWindowAppEvents(app)
|
setupMainWindowAppEvents(app)
|
||||||
setupDevAuthServer()
|
setupDevAuthServer()
|
||||||
handleDeepLinkFromArgv()
|
handleDeepLinkFromArgv()
|
||||||
@ -103,52 +56,38 @@ ipcMain.handle('os-info', () => {
|
|||||||
|
|
||||||
ipcMain.handle('auth-session-get', async () => {
|
ipcMain.handle('auth-session-get', async () => {
|
||||||
try {
|
try {
|
||||||
const storedValue = authStore.get(AUTH_SESSION_KEY)
|
if (!keytar) return null
|
||||||
return deserializeAuthSession(storedValue)
|
const raw = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[auth-session] Failed to read auth session.', e?.message || e)
|
console.warn('[keytar] Failed to read auth session.', e?.message || e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('auth-session-set', async (event, session) => {
|
ipcMain.handle('auth-session-set', async (event, session) => {
|
||||||
try {
|
try {
|
||||||
|
if (!keytar) return false
|
||||||
if (!session || typeof session !== 'object') return false
|
if (!session || typeof session !== 'object') return false
|
||||||
authStore.set(AUTH_SESSION_KEY, serializeAuthSession(session))
|
await keytar.setPassword(
|
||||||
|
KEYTAR_SERVICE,
|
||||||
|
KEYTAR_ACCOUNT,
|
||||||
|
JSON.stringify(session)
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[auth-session] Failed to write auth session.', e?.message || e)
|
console.warn('[keytar] Failed to write auth session.', e?.message || e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('auth-session-clear', async () => {
|
ipcMain.handle('auth-session-clear', async () => {
|
||||||
try {
|
try {
|
||||||
authStore.delete(AUTH_SESSION_KEY)
|
if (!keytar) return false
|
||||||
return true
|
return await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[auth-session] Failed to clear auth session.', e?.message || e)
|
console.warn('[keytar] Failed to clear auth session.', e?.message || e)
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('app-settings-get', async () => {
|
|
||||||
try {
|
|
||||||
const settings = appSettingsStore.get(APP_SETTINGS_KEY)
|
|
||||||
return settings && typeof settings === 'object' ? settings : {}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[app-settings] Failed to read settings.', e?.message || e)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('app-settings-set', async (event, settings) => {
|
|
||||||
try {
|
|
||||||
if (!settings || typeof settings !== 'object') return false
|
|
||||||
appSettingsStore.set(APP_SETTINGS_KEY, settings)
|
|
||||||
return true
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[app-settings] Failed to write settings.', e?.message || e)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
import { promises as fs } from 'fs'
|
|
||||||
import { createRequire } from 'module'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
|
||||||
const sudo = require('@vscode/sudo-prompt')
|
|
||||||
|
|
||||||
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`
|
|
||||||
|
|
||||||
const parseMacInstallerProgress = (output) => {
|
|
||||||
const lines = String(output || '').split('\n')
|
|
||||||
let percent = null
|
|
||||||
let message = 'Installing update...'
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith('installer:PHASE:')) {
|
|
||||||
message = line.slice('installer:PHASE:'.length).trim() || message
|
|
||||||
} else if (line.startsWith('installer:STATUS:')) {
|
|
||||||
const status = line.slice('installer:STATUS:'.length).trim()
|
|
||||||
if (status) message = status
|
|
||||||
} else if (line.startsWith('installer:%')) {
|
|
||||||
const value = Number.parseFloat(line.slice('installer:%'.length))
|
|
||||||
if (Number.isFinite(value)) {
|
|
||||||
percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value))
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
line.startsWith('installer: ') &&
|
|
||||||
!line.startsWith('installer:PHASE:') &&
|
|
||||||
!line.startsWith('installer:STATUS:') &&
|
|
||||||
!line.startsWith('installer:%')
|
|
||||||
) {
|
|
||||||
const text = line.slice('installer: '.length).trim()
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { percent, message }
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMacInstallSuccessful = (output) =>
|
|
||||||
/installer: The (install|upgrade) was successful\./i.test(output)
|
|
||||||
|
|
||||||
const isMacInstallFailed = (output) =>
|
|
||||||
/installer: The install failed/i.test(output)
|
|
||||||
|
|
||||||
const buildMacInstallScript = (installerPath, logPath) =>
|
|
||||||
`sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
|
|
||||||
installerPath
|
|
||||||
)} -target / -verboseR 2>&1 | /usr/bin/tee ${quoteShellArg(logPath)}`
|
|
||||||
|
|
||||||
const startMacInstallerProgressWatch = (logPath, webContents, sendProgress) => {
|
|
||||||
let installerOutput = ''
|
|
||||||
let offset = 0
|
|
||||||
let lastPercent = null
|
|
||||||
let lastMessage = null
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
try {
|
|
||||||
const stat = await fs.stat(logPath)
|
|
||||||
if (stat.size <= offset) return
|
|
||||||
|
|
||||||
const handle = await fs.open(logPath, 'r')
|
|
||||||
try {
|
|
||||||
const buffer = Buffer.alloc(stat.size - offset)
|
|
||||||
await handle.read(buffer, 0, buffer.length, offset)
|
|
||||||
offset = stat.size
|
|
||||||
installerOutput += buffer.toString('utf8')
|
|
||||||
|
|
||||||
const { percent, message } = parseMacInstallerProgress(installerOutput)
|
|
||||||
const resolvedMessage = message || 'Installing update...'
|
|
||||||
|
|
||||||
if (percent !== lastPercent || resolvedMessage !== lastMessage) {
|
|
||||||
lastPercent = percent
|
|
||||||
lastMessage = resolvedMessage
|
|
||||||
sendProgress(webContents, {
|
|
||||||
phase: 'installing',
|
|
||||||
percent,
|
|
||||||
message: resolvedMessage
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await handle.close()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.code !== 'ENOENT') {
|
|
||||||
console.error('[app-update] installer log poll error:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
poll().catch((error) => {
|
|
||||||
console.error('[app-update] installer log poll error:', error)
|
|
||||||
})
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
clearInterval(intervalId)
|
|
||||||
await poll()
|
|
||||||
return installerOutput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const launchMacInstaller = (
|
|
||||||
app,
|
|
||||||
installerPath,
|
|
||||||
webContents,
|
|
||||||
{ sendProgress, getInstallErrorMessage }
|
|
||||||
) => {
|
|
||||||
const logPath = path.join(path.dirname(installerPath), 'install.log')
|
|
||||||
const installScript = buildMacInstallScript(installerPath, logPath)
|
|
||||||
const promptName = 'farmcontrol'
|
|
||||||
|
|
||||||
sendProgress(webContents, {
|
|
||||||
phase: 'installing',
|
|
||||||
percent: 0,
|
|
||||||
message: 'Enter your Mac password when prompted.'
|
|
||||||
})
|
|
||||||
|
|
||||||
app.focus({ steal: true })
|
|
||||||
app.dock?.show()
|
|
||||||
|
|
||||||
const stopProgressWatch = startMacInstallerProgressWatch(
|
|
||||||
logPath,
|
|
||||||
webContents,
|
|
||||||
sendProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
sudo.exec(installScript, { name: promptName }, async (error, stdout, stderr) => {
|
|
||||||
const watchedOutput = await stopProgressWatch()
|
|
||||||
const output = `${stdout || ''}${stderr || ''}` || watchedOutput
|
|
||||||
|
|
||||||
await fs.unlink(logPath).catch(() => {})
|
|
||||||
|
|
||||||
if (stderr) console.error('[app-update] installer stderr:', stderr)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('[app-update] installer error:', error)
|
|
||||||
const message = getInstallErrorMessage(error, output)
|
|
||||||
sendProgress(webContents, {
|
|
||||||
phase: 'error',
|
|
||||||
percent: null,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
reject(new Error(message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMacInstallFailed(output) || !isMacInstallSuccessful(output)) {
|
|
||||||
const message = getInstallErrorMessage(null, output)
|
|
||||||
sendProgress(webContents, {
|
|
||||||
phase: 'error',
|
|
||||||
percent: null,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
reject(new Error(message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { percent, message } = parseMacInstallerProgress(output)
|
|
||||||
|
|
||||||
sendProgress(webContents, {
|
|
||||||
phase: 'installing',
|
|
||||||
percent: percent ?? 100,
|
|
||||||
message: message || 'Installation complete. Restarting Farm Control...'
|
|
||||||
})
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, ipcMain, Menu } from 'electron'
|
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||||
import path, { dirname } from 'path'
|
import path, { dirname } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
@ -6,7 +6,6 @@ const __filename = fileURLToPath(import.meta.url)
|
|||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
let win
|
let win
|
||||||
let sidebarViewMenuSections = []
|
|
||||||
|
|
||||||
const PROTOCOL_PREFIX = 'farmcontrol://'
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||||
|
|
||||||
@ -41,76 +40,6 @@ function sendNavigateToRenderer(redirectPath) {
|
|||||||
deliver()
|
deliver()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toElectronSidebarMenuItems(items = []) {
|
|
||||||
return items
|
|
||||||
.map((item) => {
|
|
||||||
if (item?.type === 'divider') {
|
|
||||||
return { type: 'separator' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuItem = {
|
|
||||||
label: item.label
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.children && Array.isArray(item.children) && item.children.length) {
|
|
||||||
menuItem.submenu = toElectronSidebarMenuItems(item.children)
|
|
||||||
} else if (item?.path) {
|
|
||||||
menuItem.click = () => sendNavigateToRenderer(item.path)
|
|
||||||
} else {
|
|
||||||
menuItem.enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuItem
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildApplicationMenuTemplate() {
|
|
||||||
const env = (process.env.NODE_ENV || 'development').trim()
|
|
||||||
const viewSubmenu = sidebarViewMenuSections.map((section) => ({
|
|
||||||
label: section.label,
|
|
||||||
submenu: toElectronSidebarMenuItems(section.items || [])
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (viewSubmenu.length === 0) {
|
|
||||||
viewSubmenu.push({ label: 'No sidebar items available', enabled: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env === 'development') {
|
|
||||||
viewSubmenu.push(
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
accelerator:
|
|
||||||
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
|
||||||
click: () => {
|
|
||||||
if (win && !win.isDestroyed()) {
|
|
||||||
win.webContents.toggleDevTools()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = [
|
|
||||||
{ role: 'fileMenu' },
|
|
||||||
{ role: 'editMenu' },
|
|
||||||
{ label: 'View', submenu: viewSubmenu },
|
|
||||||
{ role: 'windowMenu' }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
template.unshift({ role: 'appMenu' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return template
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyApplicationMenu() {
|
|
||||||
const menu = Menu.buildFromTemplate(buildApplicationMenuTemplate())
|
|
||||||
Menu.setApplicationMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleDeepLink(url) {
|
export function handleDeepLink(url) {
|
||||||
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
|
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
|
||||||
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
|
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
|
||||||
@ -149,33 +78,6 @@ function attachKeyboardShortcuts(browserWindow) {
|
|||||||
// Keyboard shortcuts for the main window can be added here if needed
|
// Keyboard shortcuts for the main window can be added here if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendNavigationGesture(browserWindow, direction) {
|
|
||||||
if (!browserWindow || browserWindow.isDestroyed()) return
|
|
||||||
browserWindow.webContents.send('navigation-gesture', direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupNavigationGestures(browserWindow) {
|
|
||||||
if (!browserWindow) return
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
browserWindow.on('swipe', (_event, direction) => {
|
|
||||||
if (direction === 'left') {
|
|
||||||
sendNavigationGesture(browserWindow, 'back')
|
|
||||||
} else if (direction === 'right') {
|
|
||||||
sendNavigationGesture(browserWindow, 'forward')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
browserWindow.on('app-command', (_event, command) => {
|
|
||||||
if (command === 'browser-backward') {
|
|
||||||
sendNavigationGesture(browserWindow, 'back')
|
|
||||||
} else if (command === 'browser-forward') {
|
|
||||||
sendNavigationGesture(browserWindow, 'forward')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupWindowEvents() {
|
function setupWindowEvents() {
|
||||||
if (!win) return
|
if (!win) return
|
||||||
win.on('maximize', () => {
|
win.on('maximize', () => {
|
||||||
@ -184,6 +86,7 @@ function setupWindowEvents() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
win.on('enter-full-screen', () => {
|
win.on('enter-full-screen', () => {
|
||||||
|
console.log('Entered fullscreen')
|
||||||
win.webContents.send('window-state', {
|
win.webContents.send('window-state', {
|
||||||
isFullScreen: true
|
isFullScreen: true
|
||||||
})
|
})
|
||||||
@ -215,7 +118,29 @@ export function createWindow() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
applyApplicationMenu()
|
// Set up custom menu bar
|
||||||
|
const env = (process.env.NODE_ENV || 'development').trim()
|
||||||
|
if (env === 'development') {
|
||||||
|
const devMenu = [
|
||||||
|
{
|
||||||
|
label: 'Developer',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Toggle Developer Tools',
|
||||||
|
accelerator:
|
||||||
|
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||||
|
click: () => {
|
||||||
|
win.webContents.toggleDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const menu = Menu.buildFromTemplate(devMenu)
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
} else {
|
||||||
|
Menu.setApplicationMenu(null)
|
||||||
|
}
|
||||||
|
|
||||||
// For development, load from localhost; for production, load the built index.html
|
// For development, load from localhost; for production, load the built index.html
|
||||||
if (process.env.ELECTRON_START_URL) {
|
if (process.env.ELECTRON_START_URL) {
|
||||||
@ -226,23 +151,12 @@ export function createWindow() {
|
|||||||
|
|
||||||
setupWindowEvents()
|
setupWindowEvents()
|
||||||
attachKeyboardShortcuts(win)
|
attachKeyboardShortcuts(win)
|
||||||
setupNavigationGestures(win)
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
win.maximize()
|
|
||||||
} else if (process.platform === 'win32') {
|
|
||||||
win.maximize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWindow() {
|
export function getWindow() {
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElectronVersion() {
|
|
||||||
return process.versions.electron
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupMainWindowIPC() {
|
export function setupMainWindowIPC() {
|
||||||
// IPC handler to get window state
|
// IPC handler to get window state
|
||||||
ipcMain.handle('window-state', () => {
|
ipcMain.handle('window-state', () => {
|
||||||
@ -256,7 +170,7 @@ export function setupMainWindowIPC() {
|
|||||||
|
|
||||||
// IPC handlers for window controls
|
// IPC handlers for window controls
|
||||||
ipcMain.on('window-control', (event, action) => {
|
ipcMain.on('window-control', (event, action) => {
|
||||||
if (!win && action !== 'quit') return
|
if (!win) return
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'minimize':
|
case 'minimize':
|
||||||
win.minimize()
|
win.minimize()
|
||||||
@ -271,17 +185,6 @@ export function setupMainWindowIPC() {
|
|||||||
case 'close':
|
case 'close':
|
||||||
win.close()
|
win.close()
|
||||||
break
|
break
|
||||||
case 'fullscreen':
|
|
||||||
win.setFullScreen(!win.isFullScreen())
|
|
||||||
break
|
|
||||||
case 'quit':
|
|
||||||
app.quit()
|
|
||||||
break
|
|
||||||
case 'toggle-devtools':
|
|
||||||
if (win && !win.isDestroyed()) {
|
|
||||||
win.webContents.toggleDevTools()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -305,18 +208,6 @@ export function setupMainWindowIPC() {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('set-sidebar-view-menu', (event, sidebarSections) => {
|
|
||||||
if (!Array.isArray(sidebarSections)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarViewMenuSections = sidebarSections
|
|
||||||
applyApplicationMenu()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('electron-version', () => getElectronVersion())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupMainWindowAppEvents(app) {
|
export function setupMainWindowAppEvents(app) {
|
||||||
@ -337,6 +228,7 @@ export function setupMainWindowAppEvents(app) {
|
|||||||
export function setupDevAuthServer() {
|
export function setupDevAuthServer() {
|
||||||
const env = (process.env.NODE_ENV || 'development').trim()
|
const env = (process.env.NODE_ENV || 'development').trim()
|
||||||
if (env == 'development') {
|
if (env == 'development') {
|
||||||
|
console.log('Starting development auth web server...')
|
||||||
import('express').then(({ default: express }) => {
|
import('express').then(({ default: express }) => {
|
||||||
const app = express()
|
const app = express()
|
||||||
const port = 3500
|
const port = 3500
|
||||||
@ -353,7 +245,11 @@ export function setupDevAuthServer() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {})
|
app.listen(port, () => {
|
||||||
|
console.log(`Dev auth server running on http://localhost:${port}`)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('Will use url scheme instead of auth server.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* HTML: <div class="loader"></div> */
|
/* HTML: <div class="loader"></div> */
|
||||||
.fc-loader {
|
.fc-loader {
|
||||||
width: 35px;
|
width: 35px;
|
||||||
|
|||||||
@ -1,442 +0,0 @@
|
|||||||
import { spawn } from 'child_process'
|
|
||||||
import { promises as fs } from 'fs'
|
|
||||||
import os from 'os'
|
|
||||||
import path from 'path'
|
|
||||||
import process from 'process'
|
|
||||||
|
|
||||||
const MSI_OLE_HEADER = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])
|
|
||||||
const DEBUG_PREFIX = '[app-update][win-progress]'
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
|
|
||||||
const debugLog = () => {}
|
|
||||||
|
|
||||||
const decodeMsiLogBuffer = (buffer) => {
|
|
||||||
if (!buffer?.length) return ''
|
|
||||||
|
|
||||||
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
||||||
debugLog('decoded MSI log as UTF-16 LE (BOM)')
|
|
||||||
return buffer.subarray(2).toString('utf16le')
|
|
||||||
}
|
|
||||||
|
|
||||||
const sample = buffer.subarray(0, Math.min(buffer.length, 64))
|
|
||||||
const looksUtf16 =
|
|
||||||
sample.length >= 4 &&
|
|
||||||
sample.filter((byte) => byte === 0).length > sample.length / 4
|
|
||||||
|
|
||||||
if (looksUtf16) {
|
|
||||||
debugLog('decoded MSI log as UTF-16 LE (heuristic)')
|
|
||||||
return buffer.toString('utf16le')
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('decoded MSI log as UTF-8')
|
|
||||||
return buffer.toString('utf8')
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatMsiActionName = (actionName) => {
|
|
||||||
const humanized = String(actionName)
|
|
||||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
if (!humanized) return 'Installing update...'
|
|
||||||
|
|
||||||
return `${humanized.charAt(0).toUpperCase()}${humanized.slice(1)}...`
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseWindowsInstallerProgress = (output) => {
|
|
||||||
const lines = String(output || '').split(/\r?\n/)
|
|
||||||
let percent = null
|
|
||||||
let message = 'Installing update...'
|
|
||||||
let totalTicks = 0
|
|
||||||
let currentTicks = 0
|
|
||||||
let actionStarts = 0
|
|
||||||
let actionEnds = 0
|
|
||||||
const matchedLines = []
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const actionStart = line.match(/^Action start \d{2}:\d{2}:\d{2}: (.+?)\./)
|
|
||||||
if (actionStart) {
|
|
||||||
actionStarts += 1
|
|
||||||
message = formatMsiActionName(actionStart[1])
|
|
||||||
matchedLines.push(`action-start:${actionStart[1]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const doingAction = line.match(/Doing action:\s*(.+)$/)
|
|
||||||
if (doingAction && !actionStart) {
|
|
||||||
message = formatMsiActionName(doingAction[1])
|
|
||||||
matchedLines.push(`doing-action:${doingAction[1]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^Action ended \d{2}:\d{2}:\d{2}: .+?\. Return value \d+\./.test(line)) {
|
|
||||||
actionEnds += 1
|
|
||||||
matchedLines.push('action-ended')
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressReset = line.match(/^\s*0\s+(\d+)\s+0(?:\s+\d+)?\s*$/)
|
|
||||||
if (progressReset) {
|
|
||||||
totalTicks = Number.parseInt(progressReset[1], 10) || 0
|
|
||||||
currentTicks = 0
|
|
||||||
matchedLines.push(`progress-reset:${totalTicks}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressIncrement = line.match(/^\s*2\s+(\d+)\s*$/)
|
|
||||||
if (progressIncrement) {
|
|
||||||
currentTicks += Number.parseInt(progressIncrement[1], 10) || 0
|
|
||||||
matchedLines.push(`progress-increment:${progressIncrement[1]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressAddition = line.match(/^\s*3\s+(\d+)\s*$/)
|
|
||||||
if (progressAddition) {
|
|
||||||
totalTicks += Number.parseInt(progressAddition[1], 10) || 0
|
|
||||||
matchedLines.push(`progress-addition:${progressAddition[1]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/Installation success or error status:\s*0\b/.test(line)) {
|
|
||||||
percent = 100
|
|
||||||
message = 'Installation complete. Restarting Farm Control...'
|
|
||||||
matchedLines.push('install-success')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
if (totalTicks > 0) {
|
|
||||||
percent = Math.min(99, Math.round((currentTicks / totalTicks) * 100))
|
|
||||||
} else if (actionStarts > 0) {
|
|
||||||
percent = Math.min(
|
|
||||||
95,
|
|
||||||
Math.max(5, Math.round((actionEnds / actionStarts) * 90))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
percent,
|
|
||||||
message,
|
|
||||||
stats: {
|
|
||||||
lineCount: lines.length,
|
|
||||||
actionStarts,
|
|
||||||
actionEnds,
|
|
||||||
totalTicks,
|
|
||||||
currentTicks,
|
|
||||||
matchedLines: matchedLines.slice(-8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWindowsInstallSuccessful = (output) =>
|
|
||||||
/Installation success or error status:\s*0\b/.test(output) ||
|
|
||||||
/MainEngineThread is returning 0\b/.test(output)
|
|
||||||
|
|
||||||
const isWindowsInstallFailed = (output) =>
|
|
||||||
/Installation success or error status:\s*[1-9]\d*\b/.test(output) ||
|
|
||||||
/MainEngineThread is returning [1-9]\d*\b/.test(output)
|
|
||||||
|
|
||||||
const isValidMsiPackage = async (filePath) => {
|
|
||||||
const handle = await fs.open(filePath, 'r')
|
|
||||||
try {
|
|
||||||
const header = Buffer.alloc(MSI_OLE_HEADER.length)
|
|
||||||
await handle.read(header, 0, header.length, 0)
|
|
||||||
return header.equals(MSI_OLE_HEADER)
|
|
||||||
} finally {
|
|
||||||
await handle.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prepareInstallerPath = async (installerPath) => {
|
|
||||||
const fileName = path.basename(installerPath)
|
|
||||||
const updateDir = path.join(
|
|
||||||
os.homedir(),
|
|
||||||
'AppData',
|
|
||||||
'Local',
|
|
||||||
'FarmControl',
|
|
||||||
'Updates'
|
|
||||||
)
|
|
||||||
await fs.mkdir(updateDir, { recursive: true })
|
|
||||||
|
|
||||||
const stablePath = path.join(updateDir, fileName)
|
|
||||||
await fs.copyFile(installerPath, stablePath)
|
|
||||||
|
|
||||||
// Resolve to a canonical long path. Short 8.3 paths (e.g. ADMINI~1) break msiexec.
|
|
||||||
const resolvedPath = await fs.realpath(stablePath)
|
|
||||||
const stats = await fs.stat(resolvedPath)
|
|
||||||
|
|
||||||
if (!stats.isFile() || stats.size === 0) {
|
|
||||||
throw new Error('Update installer file is missing or empty.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isValidMsiPackage(resolvedPath))) {
|
|
||||||
throw new Error(
|
|
||||||
'Downloaded update is not a valid Windows Installer package. The file may be corrupted or incomplete.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
|
|
||||||
let installerOutput = ''
|
|
||||||
let lastLogSize = 0
|
|
||||||
let lastPercent = null
|
|
||||||
let lastMessage = null
|
|
||||||
let pollCount = 0
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
pollCount += 1
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = await fs.stat(logPath)
|
|
||||||
if (stat.size === 0) {
|
|
||||||
debugLog(`poll #${pollCount}: log exists but is empty`, { logPath })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stat.size === lastLogSize) {
|
|
||||||
debugLog(`poll #${pollCount}: no new log data`, {
|
|
||||||
logPath,
|
|
||||||
size: stat.size
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = Buffer.alloc(stat.size)
|
|
||||||
const handle = await fs.open(logPath, 'r')
|
|
||||||
try {
|
|
||||||
await handle.read(buffer, 0, stat.size, 0)
|
|
||||||
} finally {
|
|
||||||
await handle.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
lastLogSize = stat.size
|
|
||||||
installerOutput = decodeMsiLogBuffer(buffer)
|
|
||||||
|
|
||||||
const { percent, message, stats } =
|
|
||||||
parseWindowsInstallerProgress(installerOutput)
|
|
||||||
const resolvedPercent = percent ?? lastPercent ?? 0
|
|
||||||
const resolvedMessage = message || 'Installing update...'
|
|
||||||
|
|
||||||
debugLog(`poll #${pollCount}: parsed installer log`, {
|
|
||||||
logPath,
|
|
||||||
size: stat.size,
|
|
||||||
textLength: installerOutput.length,
|
|
||||||
preview: installerOutput.slice(0, 240).replace(/\s+/g, ' '),
|
|
||||||
parsed: stats,
|
|
||||||
resolvedPercent,
|
|
||||||
resolvedMessage
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
resolvedPercent !== lastPercent ||
|
|
||||||
resolvedMessage !== lastMessage
|
|
||||||
) {
|
|
||||||
debugLog(`poll #${pollCount}: sending progress update`, {
|
|
||||||
percent: resolvedPercent,
|
|
||||||
message: resolvedMessage
|
|
||||||
})
|
|
||||||
|
|
||||||
lastPercent = resolvedPercent
|
|
||||||
lastMessage = resolvedMessage
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'installing',
|
|
||||||
percent: resolvedPercent,
|
|
||||||
message: resolvedMessage
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
debugLog(`poll #${pollCount}: progress unchanged, skipping UI update`, {
|
|
||||||
percent: resolvedPercent,
|
|
||||||
message: resolvedMessage
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.code === 'ENOENT') {
|
|
||||||
debugLog(`poll #${pollCount}: log file not created yet`, { logPath })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
poll().catch((error) => {
|
|
||||||
console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
|
|
||||||
})
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
clearInterval(intervalId)
|
|
||||||
await poll()
|
|
||||||
debugLog('stopped progress watch', {
|
|
||||||
logPath,
|
|
||||||
finalSize: lastLogSize,
|
|
||||||
textLength: installerOutput.length,
|
|
||||||
pollCount
|
|
||||||
})
|
|
||||||
return installerOutput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const launchWindowsInstaller = async (
|
|
||||||
mainWindow,
|
|
||||||
installerPath,
|
|
||||||
webContents,
|
|
||||||
{ sendProgress, getInstallErrorMessage }
|
|
||||||
) => {
|
|
||||||
const resolvedPath = await prepareInstallerPath(installerPath)
|
|
||||||
const logPath = path.join(path.dirname(resolvedPath), 'install.log')
|
|
||||||
|
|
||||||
debugLog('prepared installer', {
|
|
||||||
installerPath,
|
|
||||||
resolvedPath,
|
|
||||||
logPath
|
|
||||||
})
|
|
||||||
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'installing',
|
|
||||||
percent: 0,
|
|
||||||
message: 'Installing update...'
|
|
||||||
})
|
|
||||||
|
|
||||||
await fs.unlink(logPath).catch(() => {})
|
|
||||||
|
|
||||||
// Allow file handles from the download/copy to settle before msiexec opens the MSI.
|
|
||||||
await sleep(2000)
|
|
||||||
|
|
||||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
|
||||||
logPath,
|
|
||||||
sendProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let processOutput = ''
|
|
||||||
const startedAt = Date.now()
|
|
||||||
|
|
||||||
const installerArgs = [
|
|
||||||
'/i',
|
|
||||||
resolvedPath,
|
|
||||||
'/qn',
|
|
||||||
'/norestart',
|
|
||||||
'ALLUSERS=2',
|
|
||||||
'MSIINSTALLPERUSER=1',
|
|
||||||
'REBOOT=ReallySuppress',
|
|
||||||
'/L*v!',
|
|
||||||
logPath
|
|
||||||
]
|
|
||||||
|
|
||||||
debugLog('spawning msiexec', {
|
|
||||||
args: installerArgs,
|
|
||||||
elapsedMs: Date.now() - startedAt
|
|
||||||
})
|
|
||||||
|
|
||||||
const installerProcess = spawn('msiexec.exe', installerArgs, {
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
windowsHide: true
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.stdout?.on('data', (data) => {
|
|
||||||
const text = data.toString('utf16le')
|
|
||||||
processOutput += text
|
|
||||||
debugLog('msiexec stdout chunk', {
|
|
||||||
length: text.length,
|
|
||||||
preview: text.slice(0, 200)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.stderr?.on('data', (data) => {
|
|
||||||
const text = data.toString('utf16le')
|
|
||||||
processOutput += text
|
|
||||||
debugLog('msiexec stderr chunk', {
|
|
||||||
length: text.length,
|
|
||||||
preview: text.slice(0, 200)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.on('spawn', () => {
|
|
||||||
debugLog('msiexec spawned', {
|
|
||||||
pid: installerProcess.pid,
|
|
||||||
elapsedMs: Date.now() - startedAt
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.on('error', async (error) => {
|
|
||||||
console.error(`${DEBUG_PREFIX} installer spawn error:`, error)
|
|
||||||
const watchedOutput = await stopProgressWatch()
|
|
||||||
|
|
||||||
debugLog('installer spawn failed', {
|
|
||||||
watchedOutputLength: watchedOutput.length,
|
|
||||||
processOutputLength: processOutput.length
|
|
||||||
})
|
|
||||||
|
|
||||||
const message = error?.message || 'Failed to start update installer.'
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'error',
|
|
||||||
percent: null,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.on('exit', async (code, signal) => {
|
|
||||||
const watchedOutput = await stopProgressWatch()
|
|
||||||
const output = watchedOutput || processOutput
|
|
||||||
const finalParse = parseWindowsInstallerProgress(output)
|
|
||||||
|
|
||||||
debugLog('msiexec exited', {
|
|
||||||
code,
|
|
||||||
signal,
|
|
||||||
elapsedMs: Date.now() - startedAt,
|
|
||||||
watchedOutputLength: watchedOutput.length,
|
|
||||||
processOutputLength: processOutput.length,
|
|
||||||
parsed: finalParse.stats,
|
|
||||||
outputPreview: output.slice(0, 500).replace(/\s+/g, ' ')
|
|
||||||
})
|
|
||||||
|
|
||||||
debugLog('keeping install log', { logPath })
|
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
const message = getInstallErrorMessage(null, output)
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'error',
|
|
||||||
percent: null,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
reject(new Error(message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const succeeded =
|
|
||||||
isWindowsInstallSuccessful(output) ||
|
|
||||||
(code === 0 && !isWindowsInstallFailed(output))
|
|
||||||
|
|
||||||
debugLog('install success evaluation', {
|
|
||||||
succeeded,
|
|
||||||
isSuccessful: isWindowsInstallSuccessful(output),
|
|
||||||
isFailed: isWindowsInstallFailed(output),
|
|
||||||
exitCode: code
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!succeeded) {
|
|
||||||
const message = getInstallErrorMessage(null, output)
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'error',
|
|
||||||
percent: null,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
reject(new Error(message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { percent, message } = finalParse
|
|
||||||
|
|
||||||
sendProgress( {
|
|
||||||
phase: 'installing',
|
|
||||||
percent: percent ?? 100,
|
|
||||||
message: message || 'Installation complete. Restarting Farm Control...'
|
|
||||||
})
|
|
||||||
|
|
||||||
debugLog('installer completed successfully')
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const srcFile = path.join(rootDir, "native/macos/window-effects.mm");
|
|
||||||
export const outFile = path.join(rootDir, "src/bun/libMacWindowEffects.dylib");
|
|
||||||
const minDylibBytes = 10_000;
|
|
||||||
|
|
||||||
function normalizeArch(value) {
|
|
||||||
if (value === "x64" || value === "amd64" || value === "x86_64") {
|
|
||||||
return "x86_64";
|
|
||||||
}
|
|
||||||
if (value === "arm64" || value === "aarch64") {
|
|
||||||
return "arm64";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveTargetArch() {
|
|
||||||
return (
|
|
||||||
normalizeArch(process.env.ELECTROBUN_TARGET_ARCH) ||
|
|
||||||
normalizeArch(process.env.ELECTROBUN_ARCH) ||
|
|
||||||
normalizeArch(process.arch) ||
|
|
||||||
"arm64"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPlaceholder() {
|
|
||||||
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
||||||
writeFileSync(outFile, "");
|
|
||||||
console.log(`build-macos-effects: created placeholder dylib at ${outFile}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDylibArch(dylibPath) {
|
|
||||||
const lipo = spawnSync("lipo", ["-info", dylibPath], { encoding: "utf8" });
|
|
||||||
if (lipo.status === 0) {
|
|
||||||
return lipo.stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = spawnSync("file", ["-b", dylibPath], { encoding: "utf8" });
|
|
||||||
return file.stdout || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateMacosEffectsDylib({
|
|
||||||
dylibPath = outFile,
|
|
||||||
expectedArch = resolveTargetArch(),
|
|
||||||
required = process.platform === "darwin",
|
|
||||||
} = {}) {
|
|
||||||
if (!required) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(dylibPath)) {
|
|
||||||
console.error(`build-macos-effects: missing dylib at ${dylibPath}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { size } = statSync(dylibPath);
|
|
||||||
if (size < minDylibBytes) {
|
|
||||||
console.error(
|
|
||||||
`build-macos-effects: dylib at ${dylibPath} is too small (${size} bytes)`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const archInfo = readDylibArch(dylibPath);
|
|
||||||
if (!archInfo.includes(expectedArch)) {
|
|
||||||
console.error(
|
|
||||||
`build-macos-effects: dylib architecture mismatch (expected ${expectedArch}): ${archInfo.trim()}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
createPlaceholder();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(srcFile)) {
|
|
||||||
console.error(`build-macos-effects: missing source file ${srcFile}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
||||||
|
|
||||||
const targetArch = resolveTargetArch();
|
|
||||||
const result = spawnSync(
|
|
||||||
"xcrun",
|
|
||||||
[
|
|
||||||
"clang++",
|
|
||||||
"-dynamiclib",
|
|
||||||
"-fobjc-arc",
|
|
||||||
"-arch",
|
|
||||||
targetArch,
|
|
||||||
"-mmacosx-version-min=11.0",
|
|
||||||
"-framework",
|
|
||||||
"Cocoa",
|
|
||||||
srcFile,
|
|
||||||
"-o",
|
|
||||||
outFile,
|
|
||||||
],
|
|
||||||
{ cwd: rootDir, stdio: "inherit" },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateMacosEffectsDylib({ dylibPath: outFile, expectedArch: targetArch })) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { size } = statSync(outFile);
|
|
||||||
console.log(
|
|
||||||
`build-macos-effects: built ${outFile} (${size} bytes, arch=${targetArch})`,
|
|
||||||
);
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import path from "node:path";
|
|
||||||
import { getReleaseArch, isBundleCefEnabled } from "./release-artifact-utils.mjs";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const hostArch = getReleaseArch(process.arch);
|
|
||||||
const targetArchs = process.env.ELECTROBUN_TARGET_ARCH
|
|
||||||
? [getReleaseArch(process.env.ELECTROBUN_TARGET_ARCH)]
|
|
||||||
: ["arm64", "x64"];
|
|
||||||
const bundleCef = isBundleCefEnabled();
|
|
||||||
|
|
||||||
function canRunUnderArch(archFlag) {
|
|
||||||
const result = spawnSync("arch", [archFlag, "true"], {
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
return result.status === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(command, args, options = {}) {
|
|
||||||
const result = spawnSync(command, args, {
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: process.env,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildElectrobunEnv(targetArch) {
|
|
||||||
return {
|
|
||||||
...process.env,
|
|
||||||
ELECTROBUN_BUILD_ENV: "stable",
|
|
||||||
ELECTROBUN_OS: "macos",
|
|
||||||
ELECTROBUN_ARCH: targetArch,
|
|
||||||
ELECTROBUN_FORCE_ARCH: targetArch,
|
|
||||||
ELECTROBUN_TARGET_ARCH: targetArch,
|
|
||||||
NODE_ENV: "production",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInvocation(targetArch) {
|
|
||||||
const env = buildElectrobunEnv(targetArch);
|
|
||||||
const runScript = path.join(rootDir, "scripts/run-electrobun-build.mjs");
|
|
||||||
|
|
||||||
if (targetArch === hostArch) {
|
|
||||||
return { command: "bun", args: [runScript], env };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetArch === "x64" && hostArch === "arm64" && canRunUnderArch("-x86_64")) {
|
|
||||||
return {
|
|
||||||
command: "arch",
|
|
||||||
args: ["-x86_64", "bun", runScript],
|
|
||||||
env,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetArch === "arm64" && hostArch === "x64") {
|
|
||||||
return { command: "bun", args: [runScript], env };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderedTargets = [...targetArchs].sort((a, b) => {
|
|
||||||
if (a === hostArch) return -1;
|
|
||||||
if (b === hostArch) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Host architecture: ${hostArch}`);
|
|
||||||
console.log(`CEF bundling: ${bundleCef ? "enabled" : "disabled"}`);
|
|
||||||
console.log(`Build order: ${orderedTargets.join(", ")}`);
|
|
||||||
|
|
||||||
let builtCount = 0;
|
|
||||||
|
|
||||||
for (const targetArch of orderedTargets) {
|
|
||||||
const invocation = getInvocation(targetArch);
|
|
||||||
if (!invocation) {
|
|
||||||
console.error(
|
|
||||||
`Cannot build macOS ${targetArch} from host ${hostArch}.`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n=== Building macOS ${targetArch} (host ${hostArch}, cef=${bundleCef}) ===`);
|
|
||||||
console.log(
|
|
||||||
`ELECTROBUN_BUILD_ENV=stable ELECTROBUN_OS=macos ELECTROBUN_ARCH=${targetArch} ELECTROBUN_BUNDLE_CEF=${bundleCef}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
run(invocation.command, invocation.args, { env: invocation.env });
|
|
||||||
builtCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builtCount === 0) {
|
|
||||||
console.error("No macOS architectures were built.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builtCount < targetArchs.length) {
|
|
||||||
console.error(
|
|
||||||
`Built ${builtCount}/${targetArchs.length} macOS architectures.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nBuilt macOS architectures: ${orderedTargets.join(", ")}`);
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
|
|
||||||
const result = spawnSync("bun", ["x", "vite", "build"], {
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
NODE_ENV: process.env.NODE_ENV || "production",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncResult = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/sync-electrobun-views.mjs")],
|
|
||||||
{
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: process.env,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (syncResult.status !== 0) {
|
|
||||||
process.exit(syncResult.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`build-renderer: output at ${path.join(rootDir, "dist/mainview")}`,
|
|
||||||
);
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { existsSync, mkdirSync } from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
const entrypoint = path.join(rootDir, 'src/bun/deeplink.js')
|
|
||||||
|
|
||||||
export function stageWindowsDeeplink(appDir) {
|
|
||||||
if (!existsSync(entrypoint)) {
|
|
||||||
throw new Error(`stage-windows-deeplink: entrypoint not found: ${entrypoint}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputPath = path.join(appDir, 'bin', 'deeplink.js')
|
|
||||||
mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
||||||
|
|
||||||
const result = spawnSync(
|
|
||||||
'bun',
|
|
||||||
[
|
|
||||||
'build',
|
|
||||||
'--minify',
|
|
||||||
'--target=bun',
|
|
||||||
entrypoint,
|
|
||||||
'--outfile',
|
|
||||||
outputPath
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: process.env
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`stage-windows-deeplink: bun build failed with exit code ${result.status ?? 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(outputPath)) {
|
|
||||||
throw new Error(`stage-windows-deeplink: output not created: ${outputPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`stage-windows-deeplink: created ${outputPath}`)
|
|
||||||
return outputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
||||||
const appDirArg = process.argv[2]
|
|
||||||
if (!appDirArg) {
|
|
||||||
console.error('Usage: bun scripts/build-windows-deeplink.mjs <app-dir>')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
stageWindowsDeeplink(path.resolve(appDirArg))
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$SetupExe,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$OutputMsi,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Version,
|
|
||||||
|
|
||||||
[string]$UpgradeCode = "735812DB-E33B-57A0-8FBC-5FC3155925AA"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
function Get-MsiVersion {
|
|
||||||
param([string]$InputVersion)
|
|
||||||
|
|
||||||
$parts = $InputVersion.Split(".")
|
|
||||||
while ($parts.Count -lt 4) {
|
|
||||||
$parts += "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ($parts[0..3] -join ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-WixToolset {
|
|
||||||
$searchRoots = @(
|
|
||||||
$env:WIX,
|
|
||||||
$env:WIX_TOOLSET_PATH,
|
|
||||||
"${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin",
|
|
||||||
"${env:ProgramFiles}\WiX Toolset v3.14\bin",
|
|
||||||
"${env:ProgramFiles(x86)}\WiX Toolset v3.11\bin",
|
|
||||||
"${env:ProgramFiles}\WiX Toolset v3.11\bin"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($root in $searchRoots) {
|
|
||||||
if (-not $root) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$candle = Join-Path $root "candle.exe"
|
|
||||||
$light = Join-Path $root "light.exe"
|
|
||||||
if ((Test-Path $candle) -and (Test-Path $light)) {
|
|
||||||
return @{
|
|
||||||
Candle = $candle
|
|
||||||
Light = $light
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$candleCommand = Get-Command candle.exe -ErrorAction SilentlyContinue
|
|
||||||
$lightCommand = Get-Command light.exe -ErrorAction SilentlyContinue
|
|
||||||
if ($candleCommand -and $lightCommand) {
|
|
||||||
return @{
|
|
||||||
Candle = $candleCommand.Source
|
|
||||||
Light = $lightCommand.Source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw @"
|
|
||||||
WiX Toolset not found. Install WiX Toolset 3.11+ on the Windows build agent and ensure candle.exe and light.exe are on PATH.
|
|
||||||
Example: choco install wixtoolset --version=3.14.0.4118
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
function Escape-WixSourcePath {
|
|
||||||
param([string]$Path)
|
|
||||||
|
|
||||||
return $Path.Replace("\", "\\")
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootDir = Split-Path -Parent $PSScriptRoot
|
|
||||||
$templatePath = Join-Path $rootDir "packaging/windows/msi-wrapped.wxs"
|
|
||||||
$workDir = Join-Path $env:TEMP "farmcontrol-wix"
|
|
||||||
$wix = Find-WixToolset
|
|
||||||
|
|
||||||
if (Test-Path $workDir) {
|
|
||||||
Remove-Item $workDir -Recurse -Force
|
|
||||||
}
|
|
||||||
New-Item -ItemType Directory -Path $workDir | Out-Null
|
|
||||||
|
|
||||||
$setupExePath = (Resolve-Path -LiteralPath $SetupExe).Path
|
|
||||||
$outputMsiPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputMsi)
|
|
||||||
$outputDir = Split-Path $outputMsiPath -Parent
|
|
||||||
if ($outputDir -and -not (Test-Path $outputDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$installerIconPath = Join-Path $rootDir "assets\installer.ico"
|
|
||||||
if (-not (Test-Path $installerIconPath)) {
|
|
||||||
throw "Installer icon not found at $installerIconPath. Run 'bun run generate-app-icons' first."
|
|
||||||
}
|
|
||||||
|
|
||||||
$installerIconWorkPath = Join-Path $workDir "installer.ico"
|
|
||||||
Copy-Item -LiteralPath $installerIconPath -Destination $installerIconWorkPath -Force
|
|
||||||
|
|
||||||
$msiVersion = Get-MsiVersion $Version
|
|
||||||
$wxsContent = Get-Content -LiteralPath $templatePath -Raw
|
|
||||||
$wxsContent = $wxsContent.Replace("__UPGRADE_CODE__", $UpgradeCode)
|
|
||||||
$wxsContent = $wxsContent.Replace("__VERSION__", $msiVersion)
|
|
||||||
$wxsContent = $wxsContent.Replace("__SETUP_EXE__", (Escape-WixSourcePath $setupExePath))
|
|
||||||
$wxsContent = $wxsContent.Replace("__INSTALLER_ICON__", (Escape-WixSourcePath $installerIconWorkPath))
|
|
||||||
|
|
||||||
$projectWxs = Join-Path $workDir "project.wxs"
|
|
||||||
Set-Content -LiteralPath $projectWxs -Value $wxsContent -Encoding UTF8
|
|
||||||
|
|
||||||
$wixObj = Join-Path $workDir "project.wixobj"
|
|
||||||
|
|
||||||
Push-Location $workDir
|
|
||||||
try {
|
|
||||||
& $wix.Candle "-arch" "x64" $projectWxs
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "candle.exe failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
|
|
||||||
& $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" $wixObj
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "light.exe failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Pop-Location
|
|
||||||
Remove-Item $workDir -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $outputMsiPath)) {
|
|
||||||
throw "MSI installer was not created at $outputMsiPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Created MSI installer at $outputMsiPath"
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$AppDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$OutputExe,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Version,
|
|
||||||
|
|
||||||
[string]$BuildNumber = "dev",
|
|
||||||
|
|
||||||
[long]$MinInstallerBytes = 10485760
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
function Find-Makensis() {
|
|
||||||
$command = Get-Command makensis -ErrorAction SilentlyContinue
|
|
||||||
if ($command) {
|
|
||||||
return $command.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
$searchRoots = @(
|
|
||||||
"${env:ProgramFiles(x86)}\NSIS\makensis.exe",
|
|
||||||
"${env:ProgramFiles}\NSIS\makensis.exe"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($candidate in $searchRoots) {
|
|
||||||
if (Test-Path $candidate) {
|
|
||||||
return $candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw "Could not find makensis. Install NSIS 3.x on the Windows build agent."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-DirectoryBytes {
|
|
||||||
param([string]$Path)
|
|
||||||
|
|
||||||
$total = 0
|
|
||||||
foreach ($item in Get-ChildItem -LiteralPath $Path -Recurse -File -Force) {
|
|
||||||
$total += $item.Length
|
|
||||||
}
|
|
||||||
return $total
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootDir = Split-Path -Parent $PSScriptRoot
|
|
||||||
$nsiPath = Join-Path $rootDir "packaging/windows/farmcontrol.nsi"
|
|
||||||
$installerInclude = Join-Path $rootDir "packaging/windows/installer.nsh"
|
|
||||||
$fileListGenerator = Join-Path $rootDir "scripts/generate-nsis-file-list.ps1"
|
|
||||||
$workDir = Join-Path $env:TEMP "farmcontrol-nsis"
|
|
||||||
$localInstallerName = "farmcontrol-installer.exe"
|
|
||||||
|
|
||||||
if (Test-Path $workDir) {
|
|
||||||
Remove-Item $workDir -Recurse -Force
|
|
||||||
}
|
|
||||||
New-Item -ItemType Directory -Path $workDir | Out-Null
|
|
||||||
|
|
||||||
$appDirPath = (Resolve-Path -LiteralPath $AppDir).Path
|
|
||||||
$stagingAppDir = Join-Path $workDir "app"
|
|
||||||
|
|
||||||
Write-Host "Staging application files for NSIS from $appDirPath"
|
|
||||||
Copy-Item -LiteralPath $appDirPath -Destination $stagingAppDir -Recurse -Force
|
|
||||||
|
|
||||||
$stagedAppBytes = Get-DirectoryBytes $stagingAppDir
|
|
||||||
Write-Host "Staged application size: $([math]::Round($stagedAppBytes / 1MB, 2)) MB"
|
|
||||||
|
|
||||||
if ($stagedAppBytes -lt 5242880) {
|
|
||||||
throw "Staged application is only $([math]::Round($stagedAppBytes / 1KB, 1)) KiB. Expected a full Electrobun app bundle before building the installer."
|
|
||||||
}
|
|
||||||
|
|
||||||
$requiredExe = Join-Path $stagingAppDir "bin\FarmControl.exe"
|
|
||||||
$requiredDeeplinkScript = Join-Path $stagingAppDir "bin\deeplink.js"
|
|
||||||
if (-not (Test-Path $requiredExe)) {
|
|
||||||
throw "Staged application is missing bin\FarmControl.exe"
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $requiredDeeplinkScript)) {
|
|
||||||
throw "Staged application is missing bin\deeplink.js"
|
|
||||||
}
|
|
||||||
|
|
||||||
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")
|
|
||||||
Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh")
|
|
||||||
Copy-Item -LiteralPath $fileListGenerator -Destination (Join-Path $workDir "generate-nsis-file-list.ps1")
|
|
||||||
|
|
||||||
$iconPath = Join-Path $rootDir "assets\installer.ico"
|
|
||||||
if (Test-Path $iconPath) {
|
|
||||||
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force
|
|
||||||
Write-Host "Using installer icon from $iconPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$uninstallerIconPath = Join-Path $rootDir "assets\uninstaller.ico"
|
|
||||||
if (Test-Path $uninstallerIconPath) {
|
|
||||||
Copy-Item -LiteralPath $uninstallerIconPath -Destination (Join-Path $workDir "uninstaller.ico") -Force
|
|
||||||
Write-Host "Using uninstaller icon from $uninstallerIconPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$makensis = Find-Makensis
|
|
||||||
$outputExePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputExe)
|
|
||||||
$outputDir = Split-Path $outputExePath -Parent
|
|
||||||
if (-not (Test-Path $outputDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $outputDir | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$makensisArgs = @(
|
|
||||||
"/V3"
|
|
||||||
"/NOCD"
|
|
||||||
"/DOUTFILE=$localInstallerName"
|
|
||||||
"/DVERSION=$Version"
|
|
||||||
"/DBUILD_NUMBER=$BuildNumber"
|
|
||||||
"/DAPP_SOURCE_DIR=app"
|
|
||||||
"/DAPP_FILE_LIST_GENERATOR=generate-nsis-file-list.ps1"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Test-Path (Join-Path $workDir "icon.ico")) {
|
|
||||||
$makensisArgs += "/DINSTALLER_ICON=icon.ico"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path (Join-Path $workDir "uninstaller.ico")) {
|
|
||||||
$makensisArgs += "/DUNINSTALLER_ICON=uninstaller.ico"
|
|
||||||
}
|
|
||||||
|
|
||||||
$makensisArgs += (Join-Path $workDir "farmcontrol.nsi")
|
|
||||||
|
|
||||||
Push-Location $workDir
|
|
||||||
try {
|
|
||||||
& $makensis @makensisArgs
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "makensis failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
$localInstallerPath = Join-Path $workDir $localInstallerName
|
|
||||||
if (-not (Test-Path $localInstallerPath)) {
|
|
||||||
throw "NSIS installer was not created at $localInstallerPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$installerBytes = (Get-Item -LiteralPath $localInstallerPath).Length
|
|
||||||
Write-Host "NSIS installer size: $([math]::Round($installerBytes / 1MB, 2)) MB"
|
|
||||||
|
|
||||||
if ($installerBytes -lt $MinInstallerBytes) {
|
|
||||||
throw "NSIS installer is only $([math]::Round($installerBytes / 1KB, 1)) KiB at $localInstallerPath. The application files were not packaged. Check NSIS logs above."
|
|
||||||
}
|
|
||||||
|
|
||||||
Move-Item -LiteralPath $localInstallerPath -Destination $outputExePath -Force
|
|
||||||
|
|
||||||
Write-Host "Created NSIS installer at $outputExePath"
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { rmSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
|
|
||||||
for (const dir of ["build", "dist", "app_dist", "artifacts"]) {
|
|
||||||
rmSync(path.join(rootDir, dir), { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("clean: removed build, dist, app_dist, and artifacts");
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { existsSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
function getCodesignArgs() {
|
|
||||||
const identity = process.env.ELECTROBUN_DEVELOPER_ID || "-";
|
|
||||||
const args = ["--force", "--deep"];
|
|
||||||
|
|
||||||
if (identity !== "-") {
|
|
||||||
args.push("--options", "runtime", "--timestamp");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("--sign", identity);
|
|
||||||
return { identity, args };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function codesignMacAppBundle(appBundlePath) {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
console.log("codesign-macos-app: skipping (not macOS)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appBundlePath || !existsSync(appBundlePath)) {
|
|
||||||
throw new Error(`codesign-macos-app: app bundle not found: ${appBundlePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentsPath = path.join(appBundlePath, "Contents");
|
|
||||||
if (!existsSync(contentsPath)) {
|
|
||||||
throw new Error(`codesign-macos-app: invalid app bundle: ${appBundlePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { identity, args } = getCodesignArgs();
|
|
||||||
const result = spawnSync("codesign", [...args, appBundlePath], {
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`codesign failed with exit code ${result.status ?? 1}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const verify = spawnSync(
|
|
||||||
"codesign",
|
|
||||||
["--verify", "--deep", "--strict", "--verbose=2", appBundlePath],
|
|
||||||
{ encoding: "utf8" },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (verify.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`codesign verify failed: ${verify.stderr || verify.stdout || "unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = identity === "-" ? "ad-hoc" : identity;
|
|
||||||
console.log(`codesign-macos-app: signed ${appBundlePath} (${label})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAppBundlePath() {
|
|
||||||
const fromEnv = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH;
|
|
||||||
if (fromEnv && existsSync(fromEnv)) {
|
|
||||||
return path.resolve(fromEnv);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromArgv = process.argv[2];
|
|
||||||
if (fromArgv && existsSync(fromArgv)) {
|
|
||||||
return path.resolve(fromArgv);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invokedPath = process.argv[1]
|
|
||||||
? path.resolve(process.argv[1])
|
|
||||||
: null;
|
|
||||||
const modulePath = fileURLToPath(import.meta.url);
|
|
||||||
|
|
||||||
if (invokedPath === modulePath) {
|
|
||||||
const appBundlePath = resolveAppBundlePath();
|
|
||||||
if (!appBundlePath) {
|
|
||||||
console.error(
|
|
||||||
"codesign-macos-app: set ELECTROBUN_WRAPPER_BUNDLE_PATH or pass app bundle path",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
codesignMacAppBundle(appBundlePath);
|
|
||||||
}
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
import {
|
|
||||||
createWriteStream,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readdirSync,
|
|
||||||
unlinkSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { getReleaseArch } from "./release-artifact-utils.mjs";
|
|
||||||
import { fixMacosHeaderpad } from "./fix-macos-headerpad.mjs";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const electrobunDir = path.join(rootDir, "node_modules/electrobun");
|
|
||||||
const electrobunVersion = JSON.parse(
|
|
||||||
await Bun.file(path.join(electrobunDir, "package.json")).text(),
|
|
||||||
).version;
|
|
||||||
|
|
||||||
function getPlatformPaths(targetOS, targetArch) {
|
|
||||||
const binExt = targetOS === "win" ? ".exe" : "";
|
|
||||||
const platformDistDir = path.join(
|
|
||||||
electrobunDir,
|
|
||||||
`dist-${targetOS}-${targetArch}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
platformDistDir,
|
|
||||||
bunBinary: path.join(platformDistDir, "bun") + binExt,
|
|
||||||
bsdiff: path.join(platformDistDir, "bsdiff") + binExt,
|
|
||||||
bspatch: path.join(platformDistDir, "bspatch") + binExt,
|
|
||||||
launcher: path.join(platformDistDir, "launcher") + binExt,
|
|
||||||
nativeWrapperMacos: path.join(platformDistDir, "libNativeWrapper.dylib"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadFile(url, destination) {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(path.dirname(destination), { recursive: true });
|
|
||||||
const fileStream = createWriteStream(destination);
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
throw new Error(`No response body for ${url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
fileStream.write(Buffer.from(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
fileStream.end((error) => (error ? reject(error) : resolve()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractTarGz(tarPath, destination) {
|
|
||||||
mkdirSync(destination, { recursive: true });
|
|
||||||
const result = spawnSync("tar", ["-xzf", tarPath, "-C", destination], {
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`tar extraction failed for ${tarPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureCoreDependencies(targetOS, targetArch) {
|
|
||||||
const paths = getPlatformPaths(targetOS, targetArch);
|
|
||||||
const required = [
|
|
||||||
paths.bunBinary,
|
|
||||||
paths.bsdiff,
|
|
||||||
paths.bspatch,
|
|
||||||
paths.launcher,
|
|
||||||
paths.nativeWrapperMacos,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (required.every((filePath) => existsSync(filePath))) {
|
|
||||||
console.log(`ensure-electrobun-core: ${targetOS}-${targetArch} already present`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformName =
|
|
||||||
targetOS === "macos" ? "darwin" : targetOS === "win" ? "win" : "linux";
|
|
||||||
const url = `https://github.com/blackboardsh/electrobun/releases/download/v${electrobunVersion}/electrobun-core-${platformName}-${targetArch}.tar.gz`;
|
|
||||||
const tempFile = path.join(
|
|
||||||
electrobunDir,
|
|
||||||
`core-${targetOS}-${targetArch}-temp.tar.gz`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`ensure-electrobun-core: downloading ${targetOS}-${targetArch}`);
|
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
await downloadFile(url, tempFile);
|
|
||||||
extractTarGz(tempFile, paths.platformDistDir);
|
|
||||||
|
|
||||||
if (existsSync(tempFile)) {
|
|
||||||
unlinkSync(tempFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
const missing = required.filter((filePath) => !existsSync(filePath));
|
|
||||||
if (missing.length > 0) {
|
|
||||||
const extracted = existsSync(paths.platformDistDir)
|
|
||||||
? readdirSync(paths.platformDistDir)
|
|
||||||
: [];
|
|
||||||
throw new Error(
|
|
||||||
`Missing ${targetOS}-${targetArch} binaries after extract: ${missing.join(", ")}; extracted=${extracted.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`ensure-electrobun-core: ${targetOS}-${targetArch} ready`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetOS = "macos";
|
|
||||||
const hostArch = getReleaseArch(process.arch);
|
|
||||||
const targetArch = getReleaseArch(
|
|
||||||
process.env.ELECTROBUN_TARGET_ARCH ||
|
|
||||||
process.env.ELECTROBUN_FORCE_ARCH ||
|
|
||||||
process.arch,
|
|
||||||
);
|
|
||||||
|
|
||||||
const archesToEnsure = new Set([hostArch, targetArch]);
|
|
||||||
for (const arch of archesToEnsure) {
|
|
||||||
await ensureCoreDependencies(targetOS, arch);
|
|
||||||
// electrobun#485: x64 core binaries lack Mach-O headerpad and get corrupted
|
|
||||||
// by codesign; free a load-command slot before anything copies or signs them.
|
|
||||||
fixMacosHeaderpad(getPlatformPaths(targetOS, arch).platformDistDir);
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
import {
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readFileSync,
|
|
||||||
readdirSync,
|
|
||||||
rmSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
|
|
||||||
const targetOs = process.env.ELECTROBUN_OS || "macos";
|
|
||||||
const wrapperPath = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH;
|
|
||||||
|
|
||||||
if (buildEnv === "dev" || targetOs !== "macos") {
|
|
||||||
console.log("expand-macos-bundle: skipping (dev or non-macOS build)");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wrapperPath || !existsSync(wrapperPath)) {
|
|
||||||
console.error("expand-macos-bundle: ELECTROBUN_WRAPPER_BUNDLE_PATH not set or missing");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedWrapperPath = path.resolve(wrapperPath);
|
|
||||||
const contentsPath = path.join(resolvedWrapperPath, "Contents");
|
|
||||||
const resourcesPath = path.join(contentsPath, "Resources");
|
|
||||||
const metadataPath = path.join(resourcesPath, "metadata.json");
|
|
||||||
|
|
||||||
if (!existsSync(metadataPath)) {
|
|
||||||
console.log("expand-macos-bundle: no extractor metadata, assuming already expanded");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
||||||
const hash = metadata.hash;
|
|
||||||
const appName = metadata.name || "Farm Control";
|
|
||||||
const tarZstPath = path.resolve(resourcesPath, `${hash}.tar.zst`);
|
|
||||||
|
|
||||||
if (!existsSync(tarZstPath)) {
|
|
||||||
console.log(`expand-macos-bundle: ${hash}.tar.zst not found, skipping`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decompressTarZst(inputPath, outputPath) {
|
|
||||||
const systemZstd = spawnSync("zstd", ["--version"], { encoding: "utf8" });
|
|
||||||
if (systemZstd.status === 0) {
|
|
||||||
return spawnSync("zstd", ["-d", "-f", "-o", outputPath, inputPath], {
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostArch = process.arch === "arm64" ? "arm64" : "x64";
|
|
||||||
const zigZstd = path.join(
|
|
||||||
rootDir,
|
|
||||||
"node_modules/electrobun/dist-macos-" + hostArch,
|
|
||||||
"zig-zstd",
|
|
||||||
);
|
|
||||||
if (existsSync(zigZstd)) {
|
|
||||||
return spawnSync(
|
|
||||||
zigZstd,
|
|
||||||
["decompress", "-i", inputPath, "-o", outputPath, "--no-timing"],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
"expand-macos-bundle: zstd not found (install zstd or use electrobun dist binaries)",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workDir = path.join(tmpdir(), `farmcontrol-expand-${hash}`);
|
|
||||||
const tarPath = path.join(workDir, `${hash}.tar`);
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
mkdirSync(workDir, { recursive: true });
|
|
||||||
|
|
||||||
const decompress = decompressTarZst(tarZstPath, tarPath);
|
|
||||||
|
|
||||||
if (decompress.status !== 0) {
|
|
||||||
console.error("expand-macos-bundle: zstd decompression failed");
|
|
||||||
process.exit(decompress.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractTar = spawnSync("tar", ["-xf", tarPath, "-C", workDir], {
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (extractTar.status !== 0) {
|
|
||||||
console.error("expand-macos-bundle: tar extraction failed");
|
|
||||||
process.exit(extractTar.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const innerAppPath = path.join(workDir, `${appName}.app`);
|
|
||||||
if (!existsSync(innerAppPath)) {
|
|
||||||
const appBundle = readdirSync(workDir).find((entry) => entry.endsWith(".app"));
|
|
||||||
if (!appBundle) {
|
|
||||||
console.error("expand-macos-bundle: extracted app bundle not found");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedInnerApp = existsSync(innerAppPath)
|
|
||||||
? innerAppPath
|
|
||||||
: path.join(workDir, readdirSync(workDir).find((e) => e.endsWith(".app")));
|
|
||||||
const innerContentsPath = path.join(resolvedInnerApp, "Contents");
|
|
||||||
|
|
||||||
function replaceDirectory(sourceDir, destinationDir) {
|
|
||||||
rmSync(destinationDir, { recursive: true, force: true });
|
|
||||||
const result = spawnSync("ditto", [sourceDir, destinationDir], {
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
if (result.status !== 0) {
|
|
||||||
console.error("expand-macos-bundle: ditto copy failed");
|
|
||||||
process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceDirectory(
|
|
||||||
path.join(innerContentsPath, "MacOS"),
|
|
||||||
path.join(contentsPath, "MacOS"),
|
|
||||||
);
|
|
||||||
|
|
||||||
rmSync(resourcesPath, { recursive: true, force: true });
|
|
||||||
replaceDirectory(path.join(innerContentsPath, "Resources"), resourcesPath);
|
|
||||||
|
|
||||||
const innerFrameworks = path.join(innerContentsPath, "Frameworks");
|
|
||||||
const wrapperFrameworks = path.join(contentsPath, "Frameworks");
|
|
||||||
if (existsSync(innerFrameworks)) {
|
|
||||||
replaceDirectory(innerFrameworks, wrapperFrameworks);
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoPlistResult = spawnSync(
|
|
||||||
"ditto",
|
|
||||||
[path.join(innerContentsPath, "Info.plist"), path.join(contentsPath, "Info.plist")],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
if (infoPlistResult.status !== 0) {
|
|
||||||
console.error("expand-macos-bundle: Info.plist copy failed");
|
|
||||||
process.exit(infoPlistResult.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
spawnSync("xattr", ["-cr", resolvedWrapperPath], { stdio: "inherit" });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`expand-macos-bundle: pre-expanded ${resolvedWrapperPath}`);
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
import {
|
|
||||||
cpSync,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readdirSync,
|
|
||||||
rmSync,
|
|
||||||
statSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const electrobunDir = path.join(rootDir, "node_modules/electrobun");
|
|
||||||
|
|
||||||
function findZstdDecompressCommand() {
|
|
||||||
const hostArch = process.arch === "arm64" ? "arm64" : "x64";
|
|
||||||
const zigCandidates = [
|
|
||||||
path.join(electrobunDir, `dist-win-${hostArch}`, "zig-zstd.exe"),
|
|
||||||
path.join(electrobunDir, "dist-win-x64", "zig-zstd.exe"),
|
|
||||||
path.join(electrobunDir, `dist-macos-${hostArch}`, "zig-zstd"),
|
|
||||||
path.join(electrobunDir, "dist-macos-x64", "zig-zstd"),
|
|
||||||
path.join(electrobunDir, "dist-win-x64", "zig-zstd", "x64", "zig-zstd.exe"),
|
|
||||||
path.join(electrobunDir, "dist-win-x64", "zig-zstd", "arm64", "zig-zstd.exe"),
|
|
||||||
path.join(electrobunDir, "vendors", "zig-zstd", "x64", "zig-zstd.exe"),
|
|
||||||
path.join(electrobunDir, "vendors", "zig-zstd", "arm64", "zig-zstd.exe"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of zigCandidates) {
|
|
||||||
if (!existsSync(candidate)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: "zig-zstd",
|
|
||||||
command: candidate,
|
|
||||||
args: (inputPath, outputPath) => [
|
|
||||||
"decompress",
|
|
||||||
"-i",
|
|
||||||
inputPath,
|
|
||||||
"-o",
|
|
||||||
outputPath,
|
|
||||||
"--no-timing",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemZstd = spawnSync("zstd", ["--version"], {
|
|
||||||
encoding: "utf8",
|
|
||||||
shell: process.platform === "win32",
|
|
||||||
});
|
|
||||||
if (systemZstd.status === 0) {
|
|
||||||
return {
|
|
||||||
kind: "system-zstd",
|
|
||||||
command: "zstd",
|
|
||||||
args: (inputPath, outputPath) => [
|
|
||||||
"-d",
|
|
||||||
"-f",
|
|
||||||
"-o",
|
|
||||||
outputPath,
|
|
||||||
inputPath,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decompressTarZst(inputPath, outputPath) {
|
|
||||||
const zstd = findZstdDecompressCommand();
|
|
||||||
if (!zstd) {
|
|
||||||
throw new Error(
|
|
||||||
"expand-windows-installer: zstd not found (install zstd or use electrobun dist binaries)",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedInput = path.resolve(inputPath);
|
|
||||||
const resolvedOutput = path.resolve(outputPath);
|
|
||||||
const args = zstd.args(resolvedInput, resolvedOutput);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`expand-windows-installer: decompressing with ${zstd.command} (${zstd.kind})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = spawnSync(zstd.command, args, {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: false,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`expand-windows-installer: decompression failed (exit ${result.status ?? 1}) using ${zstd.command} ${args.join(" ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandWindowsAppFromArchive(setupArchivePath, parentDir) {
|
|
||||||
const resolvedArchive = path.resolve(setupArchivePath);
|
|
||||||
if (!existsSync(resolvedArchive)) {
|
|
||||||
throw new Error(`expand-windows-installer: archive not found: ${resolvedArchive}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workDir = path.join(path.resolve(parentDir), ".expanded-app");
|
|
||||||
const tarPath = path.join(workDir, "app.tar");
|
|
||||||
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
mkdirSync(workDir, { recursive: true });
|
|
||||||
|
|
||||||
const archiveForDecompress = path.join(workDir, "setup.tar.zst");
|
|
||||||
cpSync(resolvedArchive, archiveForDecompress);
|
|
||||||
decompressTarZst(archiveForDecompress, tarPath);
|
|
||||||
|
|
||||||
const extractTar = spawnSync("tar", ["-xf", tarPath, "-C", workDir], {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: false,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (extractTar.status !== 0) {
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
throw new Error(
|
|
||||||
`expand-windows-installer: tar extraction failed (exit ${extractTar.status ?? 1})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDir = readdirSync(workDir)
|
|
||||||
.filter((entry) => entry !== "app.tar" && entry !== "__MACOSX")
|
|
||||||
.map((entry) => path.join(workDir, entry))
|
|
||||||
.find((entryPath) => statSync(entryPath).isDirectory());
|
|
||||||
|
|
||||||
if (!appDir) {
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
throw new Error("expand-windows-installer: app folder not found in archive");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`expand-windows-installer: expanded ${appDir}`);
|
|
||||||
return appDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanExpandedWindowsApp(parentDir) {
|
|
||||||
const workDir = path.join(path.resolve(parentDir), ".expanded-app");
|
|
||||||
rmSync(workDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
@ -1,681 +0,0 @@
|
|||||||
import {
|
|
||||||
cpSync,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readdirSync,
|
|
||||||
readFileSync,
|
|
||||||
rmSync,
|
|
||||||
statSync
|
|
||||||
} from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import {
|
|
||||||
getReleaseArch,
|
|
||||||
getReleaseArtifactName,
|
|
||||||
getReleaseVersion,
|
|
||||||
isBundleCefEnabled
|
|
||||||
} from './release-artifact-utils.mjs'
|
|
||||||
import {
|
|
||||||
cleanExpandedWindowsApp,
|
|
||||||
expandWindowsAppFromArchive
|
|
||||||
} from './expand-windows-installer.mjs'
|
|
||||||
import { stageWindowsDeeplink } from './build-windows-deeplink.mjs'
|
|
||||||
import { patchWindowsBinaries } from './patch-windows-binaries.mjs'
|
|
||||||
import { codesignMacAppBundle } from './codesign-macos-app.mjs'
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
const packageJson = JSON.parse(
|
|
||||||
readFileSync(path.join(rootDir, 'package.json'), 'utf8')
|
|
||||||
)
|
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || 'stable'
|
|
||||||
const targetOs =
|
|
||||||
process.env.ELECTROBUN_OS ||
|
|
||||||
(process.platform === 'darwin'
|
|
||||||
? 'macos'
|
|
||||||
: process.platform === 'win32'
|
|
||||||
? 'win'
|
|
||||||
: process.platform === 'linux'
|
|
||||||
? 'linux'
|
|
||||||
: null)
|
|
||||||
const buildArch = getReleaseArch(process.env.ELECTROBUN_ARCH || process.arch)
|
|
||||||
const version =
|
|
||||||
process.env.ELECTROBUN_APP_VERSION || getReleaseVersion(packageJson)
|
|
||||||
const bundleCef = isBundleCefEnabled()
|
|
||||||
const artifactDir =
|
|
||||||
process.env.ELECTROBUN_ARTIFACT_DIR || path.join(rootDir, 'app_dist')
|
|
||||||
const identifier =
|
|
||||||
process.env.ELECTROBUN_APP_IDENTIFIER || 'com.tombutcher.farmcontrol'
|
|
||||||
const artifactPrefix = `farmcontrol-${version}-`
|
|
||||||
|
|
||||||
function artifactName(arch, ext) {
|
|
||||||
return getReleaseArtifactName(version, arch, ext, { cef: bundleCef })
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBuildRoot() {
|
|
||||||
const electrobunBuildDir = process.env.ELECTROBUN_BUILD_DIR
|
|
||||||
if (!electrobunBuildDir) {
|
|
||||||
return path.join(rootDir, 'build')
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseName = path.basename(electrobunBuildDir)
|
|
||||||
if (baseName.startsWith('stable-')) {
|
|
||||||
return path.dirname(electrobunBuildDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
return electrobunBuildDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkFiles(dir) {
|
|
||||||
const files = []
|
|
||||||
if (!existsSync(dir)) {
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of readdirSync(dir)) {
|
|
||||||
const fullPath = path.join(dir, entry)
|
|
||||||
const stats = statSync(fullPath)
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
files.push(...walkFiles(fullPath))
|
|
||||||
} else {
|
|
||||||
files.push(fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
function findByExtension(root, extension) {
|
|
||||||
return walkFiles(root).find((filePath) =>
|
|
||||||
filePath.toLowerCase().endsWith(extension.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMacAppBundle(arch) {
|
|
||||||
const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`)
|
|
||||||
if (!existsSync(platformDir)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const child of readdirSync(platformDir)) {
|
|
||||||
if (child.endsWith('.app')) {
|
|
||||||
return path.join(platformDir, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMacDmgSource(arch) {
|
|
||||||
const prefixedArtifact = walkFiles(artifactDir).find(
|
|
||||||
(filePath) =>
|
|
||||||
filePath.includes(`stable-macos-${arch}`) &&
|
|
||||||
filePath.toLowerCase().endsWith('.dmg')
|
|
||||||
)
|
|
||||||
if (prefixedArtifact) {
|
|
||||||
return prefixedArtifact
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildDmg = findByExtension(
|
|
||||||
path.join(getBuildRoot(), `stable-macos-${arch}`),
|
|
||||||
'.dmg'
|
|
||||||
)
|
|
||||||
if (buildDmg) {
|
|
||||||
return buildDmg
|
|
||||||
}
|
|
||||||
|
|
||||||
return findByExtension(artifactDir, '.dmg')
|
|
||||||
}
|
|
||||||
|
|
||||||
function findWindowsInstallerFiles() {
|
|
||||||
const buildRoot = getBuildRoot()
|
|
||||||
if (!existsSync(buildRoot)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of readdirSync(buildRoot)) {
|
|
||||||
if (!entry.startsWith('stable-win-')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformDir = path.join(buildRoot, entry)
|
|
||||||
const files = walkFiles(platformDir)
|
|
||||||
const setupExe = files.find((filePath) => /-setup\.exe$/i.test(filePath))
|
|
||||||
const setupArchive = files.find((filePath) =>
|
|
||||||
/-setup\.tar\.zst$/i.test(filePath)
|
|
||||||
)
|
|
||||||
const setupMetadata = files.find((filePath) =>
|
|
||||||
/-setup\.metadata\.json$/i.test(filePath)
|
|
||||||
)
|
|
||||||
const setupZip = files.find((filePath) => /-setup\.zip$/i.test(filePath))
|
|
||||||
|
|
||||||
if (setupExe && setupArchive && setupMetadata) {
|
|
||||||
return { setupExe, setupArchive, setupMetadata, setupZip }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishArtifact(sourcePath, arch, ext) {
|
|
||||||
if (!sourcePath || !existsSync(sourcePath)) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing source artifact for ${arch}.${ext}: ${sourcePath ?? 'not found'}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(artifactDir, { recursive: true })
|
|
||||||
const destination = path.join(artifactDir, artifactName(arch, ext))
|
|
||||||
cpSync(sourcePath, destination)
|
|
||||||
console.log(`Published ${destination}`)
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanStagingArtifacts(keepNames) {
|
|
||||||
if (!existsSync(artifactDir)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of readdirSync(artifactDir)) {
|
|
||||||
if (keepNames.includes(entry) || entry.startsWith(artifactPrefix)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
entry.includes('stable-macos-') ||
|
|
||||||
entry.includes('stable-win-') ||
|
|
||||||
entry.endsWith('-update.json') ||
|
|
||||||
entry.endsWith('.tar.gz')
|
|
||||||
) {
|
|
||||||
rmSync(path.join(artifactDir, entry), { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAC_DMG_ASSETS_DIR = path.join(rootDir, 'packaging/macos/dmg')
|
|
||||||
const MAC_INSTALLER_ICONSET_PATH = path.join(rootDir, 'assets/installer.iconset')
|
|
||||||
const MAC_DMG_BACKGROUND_PATH = path.join(rootDir, 'assets/dmg/background.png')
|
|
||||||
const MAC_DMG_BACKGROUND_RETINA_PATH = path.join(
|
|
||||||
rootDir,
|
|
||||||
'assets/dmg/background@2x.png'
|
|
||||||
)
|
|
||||||
const MAC_DMG_APP_NAME = 'Farm Control.app'
|
|
||||||
const MAC_DMG_WINDOW_WIDTH = 540
|
|
||||||
const MAC_DMG_WINDOW_HEIGHT = 380
|
|
||||||
|
|
||||||
function findCommand(command) {
|
|
||||||
const which = spawnSync('which', [command], { encoding: 'utf8' })
|
|
||||||
if (which.status === 0 && which.stdout.trim()) {
|
|
||||||
return which.stdout.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInstallerIcns(destinationPath) {
|
|
||||||
if (!existsSync(MAC_INSTALLER_ICONSET_PATH)) {
|
|
||||||
throw new Error(
|
|
||||||
`Installer iconset not found at ${MAC_INSTALLER_ICONSET_PATH}. Run \`bun run generate-app-icons\` first.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(path.dirname(destinationPath), { recursive: true })
|
|
||||||
rmSync(destinationPath, { force: true })
|
|
||||||
|
|
||||||
const iconutil = spawnSync(
|
|
||||||
'iconutil',
|
|
||||||
['-c', 'icns', '-o', destinationPath, MAC_INSTALLER_ICONSET_PATH],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (iconutil.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`iconutil failed to create installer icns with exit code ${iconutil.status ?? 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinationPath
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMacInstallerFileIcon(targetPath, icnsPath) {
|
|
||||||
if (!existsSync(icnsPath)) {
|
|
||||||
console.warn(
|
|
||||||
`finalize-desktop-artifacts: installer icns not found at ${icnsPath}, skipping custom icon`
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileicon = findCommand('fileicon')
|
|
||||||
if (fileicon) {
|
|
||||||
const result = spawnSync(fileicon, ['set', targetPath, icnsPath], {
|
|
||||||
stdio: 'inherit'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapedTarget = targetPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
||||||
const escapedIcon = icnsPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
||||||
const result = spawnSync(
|
|
||||||
'osascript',
|
|
||||||
[
|
|
||||||
'-e',
|
|
||||||
`tell application "Finder" to set icon of (POSIX file "${escapedTarget}") to (read (POSIX file "${escapedIcon}") as picture)`
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.status === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(
|
|
||||||
'finalize-desktop-artifacts: could not apply installer icon; install fileicon or run on macOS with Finder access'
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureMacDmgAssets() {
|
|
||||||
mkdirSync(MAC_DMG_ASSETS_DIR, { recursive: true })
|
|
||||||
|
|
||||||
const voliconPath = path.join(MAC_DMG_ASSETS_DIR, 'volicon.icns')
|
|
||||||
buildInstallerIcns(voliconPath)
|
|
||||||
|
|
||||||
if (!existsSync(MAC_DMG_BACKGROUND_PATH)) {
|
|
||||||
throw new Error(`DMG background not found at ${MAC_DMG_BACKGROUND_PATH}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(MAC_DMG_BACKGROUND_RETINA_PATH)) {
|
|
||||||
console.warn(
|
|
||||||
`finalize-desktop-artifacts: retina DMG background not found at ${MAC_DMG_BACKGROUND_RETINA_PATH}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
volicon: voliconPath,
|
|
||||||
background: MAC_DMG_BACKGROUND_PATH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findCreateDmgCommand() {
|
|
||||||
const which = spawnSync('which', ['create-dmg'], { encoding: 'utf8' })
|
|
||||||
if (which.status === 0 && which.stdout.trim()) {
|
|
||||||
return which.stdout.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const candidate of [
|
|
||||||
'/opt/homebrew/bin/create-dmg',
|
|
||||||
'/usr/local/bin/create-dmg'
|
|
||||||
]) {
|
|
||||||
if (existsSync(candidate)) {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMacDmgWithCreateDmg(
|
|
||||||
createDmg,
|
|
||||||
dmgPath,
|
|
||||||
sourceFolder,
|
|
||||||
appBundleName,
|
|
||||||
dmgAssets
|
|
||||||
) {
|
|
||||||
rmSync(dmgPath, { force: true })
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
'--volname',
|
|
||||||
'Farm Control',
|
|
||||||
'--window-pos',
|
|
||||||
'200',
|
|
||||||
'120',
|
|
||||||
'--window-size',
|
|
||||||
String(MAC_DMG_WINDOW_WIDTH),
|
|
||||||
String(MAC_DMG_WINDOW_HEIGHT),
|
|
||||||
'--icon-size',
|
|
||||||
'100',
|
|
||||||
'--icon',
|
|
||||||
appBundleName,
|
|
||||||
'130',
|
|
||||||
'212',
|
|
||||||
'--hide-extension',
|
|
||||||
appBundleName,
|
|
||||||
'--app-drop-link',
|
|
||||||
'410',
|
|
||||||
'212',
|
|
||||||
'--format',
|
|
||||||
'UDZO'
|
|
||||||
]
|
|
||||||
|
|
||||||
if (dmgAssets.volicon) {
|
|
||||||
args.push('--volicon', dmgAssets.volicon)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dmgAssets.background) {
|
|
||||||
args.push('--background', dmgAssets.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(dmgPath, sourceFolder)
|
|
||||||
|
|
||||||
const result = spawnSync(createDmg, args, { stdio: 'inherit' })
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`create-dmg failed with exit code ${result.status ?? 1}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(dmgPath)) {
|
|
||||||
throw new Error(`create-dmg did not produce ${dmgPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dmgPath
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyMacAppBundle(sourcePath, destinationPath) {
|
|
||||||
rmSync(destinationPath, { recursive: true, force: true })
|
|
||||||
mkdirSync(path.dirname(destinationPath), { recursive: true })
|
|
||||||
|
|
||||||
const result = spawnSync('ditto', [sourcePath, destinationPath], {
|
|
||||||
stdio: 'inherit'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`ditto failed with exit code ${result.status ?? 1}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canUseDirectDmgSourceFolder(appBundlePath) {
|
|
||||||
const platformDir = path.dirname(appBundlePath)
|
|
||||||
const appName = path.basename(appBundlePath)
|
|
||||||
const entries = readdirSync(platformDir).filter(
|
|
||||||
(entry) => !entry.startsWith('.') && entry !== '.finalize-dmg-staging'
|
|
||||||
)
|
|
||||||
|
|
||||||
return entries.length === 1 && entries[0] === appName
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildMacDmg(appBundlePath, arch) {
|
|
||||||
const createDmg = findCreateDmgCommand()
|
|
||||||
if (!createDmg) {
|
|
||||||
throw new Error(
|
|
||||||
'create-dmg not found. Install with: brew install create-dmg'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const appBundleName = path.basename(appBundlePath)
|
|
||||||
if (appBundleName !== MAC_DMG_APP_NAME) {
|
|
||||||
console.warn(
|
|
||||||
`finalize-desktop-artifacts: expected ${MAC_DMG_APP_NAME}, found ${appBundleName}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmgPath = path.join(artifactDir, artifactName(arch, 'dmg'))
|
|
||||||
|
|
||||||
mkdirSync(artifactDir, { recursive: true })
|
|
||||||
|
|
||||||
const stagingDir = path.join(
|
|
||||||
path.dirname(appBundlePath),
|
|
||||||
'.finalize-dmg-staging'
|
|
||||||
)
|
|
||||||
const useDirectSource = canUseDirectDmgSourceFolder(appBundlePath)
|
|
||||||
const sourceFolder = useDirectSource
|
|
||||||
? path.dirname(appBundlePath)
|
|
||||||
: stagingDir
|
|
||||||
|
|
||||||
if (!useDirectSource) {
|
|
||||||
rmSync(stagingDir, { recursive: true, force: true })
|
|
||||||
mkdirSync(stagingDir, { recursive: true })
|
|
||||||
const stagedAppPath = path.join(stagingDir, path.basename(appBundlePath))
|
|
||||||
copyMacAppBundle(appBundlePath, stagedAppPath)
|
|
||||||
codesignMacAppBundle(stagedAppPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let builtDmgPath
|
|
||||||
const dmgAssets = ensureMacDmgAssets()
|
|
||||||
|
|
||||||
try {
|
|
||||||
builtDmgPath = buildMacDmgWithCreateDmg(
|
|
||||||
createDmg,
|
|
||||||
dmgPath,
|
|
||||||
sourceFolder,
|
|
||||||
appBundleName,
|
|
||||||
dmgAssets
|
|
||||||
)
|
|
||||||
applyMacInstallerFileIcon(builtDmgPath, dmgAssets.volicon)
|
|
||||||
} finally {
|
|
||||||
if (!useDirectSource) {
|
|
||||||
rmSync(stagingDir, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Published ${builtDmgPath}`)
|
|
||||||
return builtDmgPath
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanMacBuildDir(arch) {
|
|
||||||
const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`)
|
|
||||||
if (existsSync(platformDir)) {
|
|
||||||
rmSync(platformDir, { recursive: true, force: true })
|
|
||||||
console.log(`Removed build output ${platformDir}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMacPkg(appBundlePath, arch) {
|
|
||||||
const pkgPath = path.join(artifactDir, artifactName(arch, 'pkg'))
|
|
||||||
|
|
||||||
const result = spawnSync(
|
|
||||||
'pkgbuild',
|
|
||||||
[
|
|
||||||
'--component',
|
|
||||||
appBundlePath,
|
|
||||||
'--install-location',
|
|
||||||
'/Applications',
|
|
||||||
'--identifier',
|
|
||||||
identifier,
|
|
||||||
'--version',
|
|
||||||
version,
|
|
||||||
pkgPath
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`pkgbuild failed with exit code ${result.status ?? 1}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Published ${pkgPath}`)
|
|
||||||
return pkgPath
|
|
||||||
}
|
|
||||||
|
|
||||||
function stageWindowsDeeplinkScript(appDir) {
|
|
||||||
return stageWindowsDeeplink(appDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanWinBuildDir(arch) {
|
|
||||||
const platformDir = path.join(getBuildRoot(), `stable-win-${arch}`)
|
|
||||||
if (existsSync(platformDir)) {
|
|
||||||
rmSync(platformDir, { recursive: true, force: true })
|
|
||||||
console.log(`Removed build output ${platformDir}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWindowsNsis(appDir, arch) {
|
|
||||||
const scriptPath = path.join(rootDir, 'scripts/build-windows-nsis.ps1')
|
|
||||||
const exePath = path.join(artifactDir, artifactName(arch, 'exe'))
|
|
||||||
const buildInfoPath = path.join(rootDir, 'src/buildInfo.json')
|
|
||||||
const buildInfo = existsSync(buildInfoPath)
|
|
||||||
? JSON.parse(readFileSync(buildInfoPath, 'utf8'))
|
|
||||||
: {}
|
|
||||||
const buildNumber =
|
|
||||||
process.env.BUILD_NUMBER ||
|
|
||||||
process.env.VITE_BUILD_NUMBER ||
|
|
||||||
buildInfo.buildNumber ||
|
|
||||||
'dev'
|
|
||||||
const powershell = process.env.SystemRoot
|
|
||||||
? path.join(
|
|
||||||
process.env.SystemRoot,
|
|
||||||
'System32',
|
|
||||||
'WindowsPowerShell',
|
|
||||||
'v1.0',
|
|
||||||
'powershell.exe'
|
|
||||||
)
|
|
||||||
: 'powershell.exe'
|
|
||||||
|
|
||||||
const result = spawnSync(
|
|
||||||
powershell,
|
|
||||||
[
|
|
||||||
'-NoProfile',
|
|
||||||
'-ExecutionPolicy',
|
|
||||||
'Bypass',
|
|
||||||
'-File',
|
|
||||||
scriptPath,
|
|
||||||
'-AppDir',
|
|
||||||
appDir,
|
|
||||||
'-OutputExe',
|
|
||||||
exePath,
|
|
||||||
'-Version',
|
|
||||||
version,
|
|
||||||
'-BuildNumber',
|
|
||||||
buildNumber
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`build-windows-nsis.ps1 failed with exit code ${result.status ?? 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const minInstallerBytes = 10 * 1024 * 1024
|
|
||||||
const installerSize = statSync(exePath).size
|
|
||||||
if (installerSize < minInstallerBytes) {
|
|
||||||
throw new Error(
|
|
||||||
`Windows installer ${path.basename(exePath)} is only ${(installerSize / 1024).toFixed(1)} KiB; expected at least ${minInstallerBytes / (1024 * 1024)} MiB`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Published ${exePath}`)
|
|
||||||
return exePath
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWindowsMsi(setupExePath, arch) {
|
|
||||||
const scriptPath = path.join(rootDir, 'scripts/build-windows-msi.ps1')
|
|
||||||
const msiPath = path.join(artifactDir, artifactName(arch, 'msi'))
|
|
||||||
const powershell = process.env.SystemRoot
|
|
||||||
? path.join(
|
|
||||||
process.env.SystemRoot,
|
|
||||||
'System32',
|
|
||||||
'WindowsPowerShell',
|
|
||||||
'v1.0',
|
|
||||||
'powershell.exe'
|
|
||||||
)
|
|
||||||
: 'powershell.exe'
|
|
||||||
|
|
||||||
const result = spawnSync(
|
|
||||||
powershell,
|
|
||||||
[
|
|
||||||
'-NoProfile',
|
|
||||||
'-ExecutionPolicy',
|
|
||||||
'Bypass',
|
|
||||||
'-File',
|
|
||||||
scriptPath,
|
|
||||||
'-SetupExe',
|
|
||||||
setupExePath,
|
|
||||||
'-OutputMsi',
|
|
||||||
msiPath,
|
|
||||||
'-Version',
|
|
||||||
version
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`build-windows-msi.ps1 failed with exit code ${result.status ?? 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Published ${msiPath}`)
|
|
||||||
return msiPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildEnv === 'dev') {
|
|
||||||
console.log('finalize-desktop-artifacts: skipping dev build')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log(
|
|
||||||
`finalize-desktop-artifacts: os=${targetOs} arch=${buildArch} cef=${bundleCef} version=${version}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (targetOs === 'macos') {
|
|
||||||
const appBundle = findMacAppBundle(buildArch)
|
|
||||||
|
|
||||||
if (!appBundle) {
|
|
||||||
console.log(
|
|
||||||
`finalize-desktop-artifacts: no macOS ${buildArch} app bundle found, skipping`
|
|
||||||
)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
codesignMacAppBundle(appBundle)
|
|
||||||
|
|
||||||
const existingDmg = findMacDmgSource(buildArch)
|
|
||||||
const dmgPath = existingDmg
|
|
||||||
? publishArtifact(existingDmg, buildArch, 'dmg')
|
|
||||||
: await buildMacDmg(appBundle, buildArch)
|
|
||||||
|
|
||||||
const published = [dmgPath, buildMacPkg(appBundle, buildArch)]
|
|
||||||
|
|
||||||
cleanStagingArtifacts(published.map((filePath) => path.basename(filePath)))
|
|
||||||
cleanMacBuildDir(buildArch)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetOs === 'win') {
|
|
||||||
const arch = getReleaseArch(process.env.ELECTROBUN_ARCH || 'x64')
|
|
||||||
const installerFiles = findWindowsInstallerFiles()
|
|
||||||
|
|
||||||
if (!installerFiles?.setupArchive) {
|
|
||||||
throw new Error(
|
|
||||||
'Could not find the Windows setup archive (.tar.zst) to expand'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformDir = path.dirname(installerFiles.setupArchive)
|
|
||||||
const appDir = expandWindowsAppFromArchive(
|
|
||||||
installerFiles.setupArchive,
|
|
||||||
platformDir
|
|
||||||
)
|
|
||||||
stageWindowsDeeplinkScript(appDir)
|
|
||||||
patchWindowsBinaries(appDir)
|
|
||||||
|
|
||||||
let published
|
|
||||||
try {
|
|
||||||
const setupExe = buildWindowsNsis(appDir, arch)
|
|
||||||
published = [setupExe, buildWindowsMsi(setupExe, arch)]
|
|
||||||
|
|
||||||
if (installerFiles.setupZip) {
|
|
||||||
published.push(publishArtifact(installerFiles.setupZip, arch, 'zip'))
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cleanExpandedWindowsApp(platformDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanStagingArtifacts(published.map((filePath) => path.basename(filePath)))
|
|
||||||
cleanWinBuildDir(arch)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'finalize-desktop-artifacts: no desktop packaging configured for',
|
|
||||||
targetOs ?? 'unknown target'
|
|
||||||
)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
await main()
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
// Workaround for https://github.com/blackboardsh/electrobun/issues/485
|
|
||||||
//
|
|
||||||
// Electrobun's darwin-x64 core binaries (launcher, extractor, libasar.dylib,
|
|
||||||
// bsdiff, zig-asar in v1.18.1) are Zig-built with zero/near-zero Mach-O
|
|
||||||
// headerpad and no code signature. When codesign adds the 16-byte
|
|
||||||
// LC_CODE_SIGNATURE load command it silently overwrites the first bytes of
|
|
||||||
// __text, and the signed binary segfaults on launch (Intel Macs only;
|
|
||||||
// arm64 binaries always ship with a signature slot that is re-signed in
|
|
||||||
// place).
|
|
||||||
//
|
|
||||||
// This script frees room in the load-command area by dropping the 16-byte
|
|
||||||
// LC_SOURCE_VERSION command (LC_UUID as a fallback) from any thin x86_64
|
|
||||||
// Mach-O that has less than 16 bytes of headerpad and no existing code
|
|
||||||
// signature. It is idempotent and a no-op once upstream ships rebuilt
|
|
||||||
// binaries with headerpad_size set.
|
|
||||||
|
|
||||||
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const MH_MAGIC_64 = 0xfeedfacf;
|
|
||||||
const CPU_TYPE_X86_64 = 0x01000007;
|
|
||||||
const LC_SEGMENT_64 = 0x19;
|
|
||||||
const LC_UUID = 0x1b;
|
|
||||||
const LC_CODE_SIGNATURE = 0x1d;
|
|
||||||
const LC_SOURCE_VERSION = 0x2a;
|
|
||||||
const HEADER_SIZE = 32;
|
|
||||||
const CODE_SIGNATURE_CMD_SIZE = 16;
|
|
||||||
|
|
||||||
function analyzeMachO(buffer) {
|
|
||||||
if (buffer.length < HEADER_SIZE || buffer.readUInt32LE(0) !== MH_MAGIC_64) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cputype = buffer.readInt32LE(4);
|
|
||||||
const ncmds = buffer.readUInt32LE(16);
|
|
||||||
const sizeofcmds = buffer.readUInt32LE(20);
|
|
||||||
|
|
||||||
let offset = HEADER_SIZE;
|
|
||||||
let firstSectionOffset = null;
|
|
||||||
let hasCodeSignature = false;
|
|
||||||
let sourceVersionCmd = null;
|
|
||||||
let uuidCmd = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < ncmds; i += 1) {
|
|
||||||
const cmd = buffer.readUInt32LE(offset);
|
|
||||||
const cmdsize = buffer.readUInt32LE(offset + 4);
|
|
||||||
|
|
||||||
if (cmd === LC_CODE_SIGNATURE) {
|
|
||||||
hasCodeSignature = true;
|
|
||||||
} else if (cmd === LC_SOURCE_VERSION) {
|
|
||||||
sourceVersionCmd = { offset, cmdsize };
|
|
||||||
} else if (cmd === LC_UUID) {
|
|
||||||
uuidCmd = { offset, cmdsize };
|
|
||||||
} else if (cmd === LC_SEGMENT_64) {
|
|
||||||
const nsects = buffer.readUInt32LE(offset + 64);
|
|
||||||
let sectionOffset = offset + 72;
|
|
||||||
for (let s = 0; s < nsects; s += 1) {
|
|
||||||
const size = Number(buffer.readBigUInt64LE(sectionOffset + 40));
|
|
||||||
const fileOffset = buffer.readUInt32LE(sectionOffset + 48);
|
|
||||||
if (fileOffset > 0 && size > 0) {
|
|
||||||
firstSectionOffset =
|
|
||||||
firstSectionOffset === null
|
|
||||||
? fileOffset
|
|
||||||
: Math.min(firstSectionOffset, fileOffset);
|
|
||||||
}
|
|
||||||
sectionOffset += 80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += cmdsize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cputype,
|
|
||||||
ncmds,
|
|
||||||
sizeofcmds,
|
|
||||||
headerpad:
|
|
||||||
firstSectionOffset === null
|
|
||||||
? Infinity
|
|
||||||
: firstSectionOffset - (HEADER_SIZE + sizeofcmds),
|
|
||||||
hasCodeSignature,
|
|
||||||
removableCmd: sourceVersionCmd ?? uuidCmd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixFile(filePath) {
|
|
||||||
const buffer = readFileSync(filePath);
|
|
||||||
const info = analyzeMachO(buffer);
|
|
||||||
const name = path.basename(filePath);
|
|
||||||
|
|
||||||
if (!info || info.cputype !== CPU_TYPE_X86_64) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.hasCodeSignature || info.headerpad >= CODE_SIGNATURE_CMD_SIZE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.removableCmd) {
|
|
||||||
console.warn(
|
|
||||||
`fix-macos-headerpad: ${name} has headerpad ${info.headerpad} but no removable load command; signing may corrupt it`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { offset, cmdsize } = info.removableCmd;
|
|
||||||
const loadCommandsEnd = HEADER_SIZE + info.sizeofcmds;
|
|
||||||
|
|
||||||
// Shift the remaining load commands over the removed one, then zero the
|
|
||||||
// freed tail so codesign finds clean padding.
|
|
||||||
buffer.copyWithin(offset, offset + cmdsize, loadCommandsEnd);
|
|
||||||
buffer.fill(0, loadCommandsEnd - cmdsize, loadCommandsEnd);
|
|
||||||
buffer.writeUInt32LE(info.ncmds - 1, 16);
|
|
||||||
buffer.writeUInt32LE(info.sizeofcmds - cmdsize, 20);
|
|
||||||
|
|
||||||
writeFileSync(filePath, buffer);
|
|
||||||
console.log(
|
|
||||||
`fix-macos-headerpad: ${name} freed ${cmdsize} bytes for LC_CODE_SIGNATURE (headerpad was ${info.headerpad})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fixMacosHeaderpad(directory) {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries;
|
|
||||||
try {
|
|
||||||
entries = readdirSync(directory);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const filePath = path.join(directory, entry);
|
|
||||||
if (statSync(filePath).isFile()) {
|
|
||||||
fixFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
||||||
const modulePath = fileURLToPath(import.meta.url);
|
|
||||||
|
|
||||||
if (invokedPath === modulePath) {
|
|
||||||
const rootDir = path.resolve(path.dirname(modulePath), "..");
|
|
||||||
const target =
|
|
||||||
process.argv[2] ||
|
|
||||||
path.join(rootDir, "node_modules/electrobun/dist-macos-x64");
|
|
||||||
fixMacosHeaderpad(path.resolve(target));
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import path from 'node:path'
|
|
||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
[
|
|
||||||
path.join(rootDir, 'scripts/prepare-app-icons.mjs'),
|
|
||||||
'assets/logos/farmcontrolicon.png'
|
|
||||||
],
|
|
||||||
[path.join(rootDir, 'scripts/prepare-installer-icons.mjs')]
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const args of steps) {
|
|
||||||
const result = spawnSync('bun', args, {
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: process.env
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
process.exit(result.status ?? 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('generate-app-icons: done')
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$SourceDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$OutputPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
function ConvertTo-NsisString {
|
|
||||||
param([string]$Value)
|
|
||||||
|
|
||||||
return $Value.Replace('$', '$$').Replace('"', '$\"')
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceRoot = (Resolve-Path -LiteralPath $SourceDir).Path.TrimEnd('\')
|
|
||||||
$sourcePrefix = "$sourceRoot\"
|
|
||||||
$files = @(
|
|
||||||
Get-ChildItem -LiteralPath $sourceRoot -Recurse -File -Force |
|
|
||||||
Sort-Object FullName
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($files.Count -eq 0) {
|
|
||||||
throw "No files were found in NSIS application source directory: $sourceRoot"
|
|
||||||
}
|
|
||||||
|
|
||||||
[long]$totalBytes = ($files | Measure-Object -Property Length -Sum).Sum
|
|
||||||
$lines = [System.Collections.Generic.List[string]]::new()
|
|
||||||
$lines.Add("!define APP_COPY_TOTAL_BYTES `"$totalBytes`"")
|
|
||||||
$lines.Add("!macro copyApplicationFilesWithProgress")
|
|
||||||
|
|
||||||
$lastRelativeDirectory = $null
|
|
||||||
foreach ($file in $files) {
|
|
||||||
$relativePath = $file.FullName.Substring($sourcePrefix.Length)
|
|
||||||
$relativeDirectory = Split-Path -Parent $relativePath
|
|
||||||
$escapedRelativePath = ConvertTo-NsisString $relativePath
|
|
||||||
$escapedSourcePath = ConvertTo-NsisString $file.FullName
|
|
||||||
$escapedFileName = ConvertTo-NsisString $file.Name
|
|
||||||
|
|
||||||
if ($relativeDirectory -ne $lastRelativeDirectory) {
|
|
||||||
if ([string]::IsNullOrEmpty($relativeDirectory)) {
|
|
||||||
$lines.Add(' SetOutPath "$INSTDIR"')
|
|
||||||
} else {
|
|
||||||
$escapedDirectory = ConvertTo-NsisString $relativeDirectory
|
|
||||||
$lines.Add(" SetOutPath `"`$INSTDIR\$escapedDirectory`"")
|
|
||||||
}
|
|
||||||
$lastRelativeDirectory = $relativeDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines.Add(" File `"/oname=$escapedFileName`" `"$escapedSourcePath`"")
|
|
||||||
$lines.Add(
|
|
||||||
" !insertmacro progressCopyFile `"$($file.Length)`" `"$escapedRelativePath`""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines.Add("!macroend")
|
|
||||||
|
|
||||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
|
||||||
[System.IO.File]::WriteAllLines($OutputPath, $lines, $encoding)
|
|
||||||
|
|
||||||
Write-Host "Generated NSIS commands for $($files.Count) files ($totalBytes bytes)."
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
!macro customInstall
|
!macro customInstall
|
||||||
DetailPrint "Register farmcontrol URI Handler"
|
DetailPrint "Register farmcontrol URI Handler"
|
||||||
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
DeleteRegKey HKCR "farmcontrol"
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol"
|
WriteRegStr HKCR "farmcontrol" "" "URL:farmcontrol"
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol" "URL Protocol" ""
|
WriteRegStr HKCR "farmcontrol" "URL Protocol" ""
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
WriteRegStr HKCR "farmcontrol\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell" "" ""
|
WriteRegStr HKCR "farmcontrol\shell" "" ""
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" ""
|
WriteRegStr HKCR "farmcontrol\shell\Open" "" ""
|
||||||
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
|
WriteRegStr HKCR "farmcontrol\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
|
||||||
!macroend
|
!macroend
|
||||||
@ -1,350 +0,0 @@
|
|||||||
import {
|
|
||||||
cpSync,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readFileSync,
|
|
||||||
writeFileSync
|
|
||||||
} from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
const electrobunDir = path.join(rootDir, 'node_modules/electrobun')
|
|
||||||
const sharedSrcDir = path.join(electrobunDir, 'src/shared')
|
|
||||||
const sharedDistDir = path.join(electrobunDir, 'dist/api/shared')
|
|
||||||
|
|
||||||
if (!existsSync(sharedDistDir)) {
|
|
||||||
console.error('patch-electrobun-src: electrobun dist/api/shared not found')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(sharedSrcDir, { recursive: true })
|
|
||||||
|
|
||||||
for (const fileName of [
|
|
||||||
'cef-version.ts',
|
|
||||||
'bun-version.ts',
|
|
||||||
'electrobun-version.ts',
|
|
||||||
'naming.ts',
|
|
||||||
'rpc.ts'
|
|
||||||
]) {
|
|
||||||
const source = path.join(sharedDistDir, fileName)
|
|
||||||
const destination = path.join(sharedSrcDir, fileName)
|
|
||||||
if (existsSync(source)) {
|
|
||||||
cpSync(source, destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformPatch = `import { platform, arch } from "os";
|
|
||||||
|
|
||||||
export type SupportedOS = "macos" | "win" | "linux";
|
|
||||||
export type SupportedArch = "arm64" | "x64";
|
|
||||||
|
|
||||||
const platformName = platform();
|
|
||||||
const archName = arch();
|
|
||||||
|
|
||||||
export const OS: SupportedOS = (() => {
|
|
||||||
switch (platformName) {
|
|
||||||
case "win32":
|
|
||||||
return "win";
|
|
||||||
case "darwin":
|
|
||||||
return "macos";
|
|
||||||
case "linux":
|
|
||||||
return "linux";
|
|
||||||
default:
|
|
||||||
throw new Error(\`Unsupported platform: \${platformName}\`);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function normalizeForcedArch(value) {
|
|
||||||
if (value === "arm64" || value === "aarch64") {
|
|
||||||
return "arm64";
|
|
||||||
}
|
|
||||||
if (value === "x64" || value === "amd64") {
|
|
||||||
return "x64";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ARCH: SupportedArch = (() => {
|
|
||||||
const forcedArch = normalizeForcedArch(
|
|
||||||
process.env.ELECTROBUN_FORCE_ARCH || process.env.ELECTROBUN_TARGET_ARCH,
|
|
||||||
);
|
|
||||||
if (forcedArch) {
|
|
||||||
return forcedArch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OS === "win") {
|
|
||||||
return "x64";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (archName) {
|
|
||||||
case "arm64":
|
|
||||||
return "arm64";
|
|
||||||
case "x64":
|
|
||||||
return "x64";
|
|
||||||
default:
|
|
||||||
throw new Error(\`Unsupported architecture: \${archName}\`);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
export const HOST_ARCH: SupportedArch = (() => {
|
|
||||||
if (OS === "win") {
|
|
||||||
return "x64";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (archName) {
|
|
||||||
case "arm64":
|
|
||||||
return "arm64";
|
|
||||||
case "x64":
|
|
||||||
return "x64";
|
|
||||||
default:
|
|
||||||
throw new Error(\`Unsupported architecture: \${archName}\`);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
export function getPlatformOS(): SupportedOS {
|
|
||||||
return OS;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPlatformArch(): SupportedArch {
|
|
||||||
return ARCH;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch)
|
|
||||||
|
|
||||||
const templatesDir = path.join(electrobunDir, 'src/cli/templates')
|
|
||||||
mkdirSync(templatesDir, { recursive: true })
|
|
||||||
writeFileSync(
|
|
||||||
path.join(templatesDir, 'embedded.ts'),
|
|
||||||
`export function getTemplateNames() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTemplate(name: string) {
|
|
||||||
throw new Error(\`Template not available in patched electrobun CLI: \${name}\`);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
|
|
||||||
const candidates = [
|
|
||||||
join(projectRoot, "node_modules/rcedit/package.json"),
|
|
||||||
join(projectRoot, "node_modules/electrobun/node_modules/rcedit/package.json"),
|
|
||||||
];
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (existsSync(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
"rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).",
|
|
||||||
);
|
|
||||||
}`
|
|
||||||
|
|
||||||
const cliPath = path.join(electrobunDir, 'src/cli/index.ts')
|
|
||||||
let cliSource = readFileSync(cliPath, 'utf8')
|
|
||||||
if (!cliSource.includes('HOST_ARCH')) {
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'import { OS, ARCH } from "../shared/platform";',
|
|
||||||
'import { OS, ARCH, HOST_ARCH } from "../shared/platform";'
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'const hostPaths = getPlatformPaths(OS, ARCH);',
|
|
||||||
'const hostPaths = getPlatformPaths(OS, HOST_ARCH);'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!cliSource.includes('toolPaths')) {
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);',
|
|
||||||
'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);'
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replaceAll(
|
|
||||||
'const zstdPath = targetPaths.ZSTD;',
|
|
||||||
'const zstdPath = toolPaths.ZSTD;'
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replaceAll(
|
|
||||||
'const bsdiffpath = targetPaths.BSDIFF;',
|
|
||||||
'const bsdiffpath = toolPaths.BSDIFF;'
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");',
|
|
||||||
'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");'
|
|
||||||
)
|
|
||||||
writeFileSync(cliPath, cliSource)
|
|
||||||
cliSource = readFileSync(cliPath, 'utf8')
|
|
||||||
}
|
|
||||||
if (!cliSource.includes('hookBunBinary')) {
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
`const hostPaths = getPlatformPaths(OS, HOST_ARCH);
|
|
||||||
|
|
||||||
const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript],`,
|
|
||||||
`const hostPaths = getPlatformPaths(OS, HOST_ARCH);
|
|
||||||
const hookBunBinary = existsSync(hostPaths.BUN_BINARY)
|
|
||||||
? hostPaths.BUN_BINARY
|
|
||||||
: process.execPath;
|
|
||||||
|
|
||||||
const result = Bun.spawnSync([hookBunBinary, hookScript],`
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);',
|
|
||||||
'console.error("Tried to run with bun at:", hookBunBinary);'
|
|
||||||
)
|
|
||||||
writeFileSync(cliPath, cliSource)
|
|
||||||
cliSource = readFileSync(cliPath, 'utf8')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cliSource.includes('resolveProjectRceditPkgPath')) {
|
|
||||||
cliSource = cliSource.replaceAll(
|
|
||||||
'const rceditPkgPath = require.resolve("rcedit/package.json");',
|
|
||||||
'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);'
|
|
||||||
)
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
'function getPlatformPaths(',
|
|
||||||
`${RCEDIT_RESOLVER_FN}
|
|
||||||
|
|
||||||
function getPlatformPaths(`
|
|
||||||
)
|
|
||||||
writeFileSync(cliPath, cliSource)
|
|
||||||
cliSource = readFileSync(cliPath, 'utf8')
|
|
||||||
} else {
|
|
||||||
cliSource = cliSource.replace(
|
|
||||||
/function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m,
|
|
||||||
RCEDIT_RESOLVER_FN
|
|
||||||
)
|
|
||||||
writeFileSync(cliPath, cliSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs')
|
|
||||||
let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8')
|
|
||||||
|
|
||||||
if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) {
|
|
||||||
electrobunCjs = electrobunCjs.replace(
|
|
||||||
`async function main() {
|
|
||||||
try {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const cliPath = await ensureCliBinary();
|
|
||||||
|
|
||||||
// Replace this process with the actual CLI
|
|
||||||
const child = spawn(cliPath, args, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: process.cwd()
|
|
||||||
});`,
|
|
||||||
`async function main() {
|
|
||||||
try {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const patchedCliPath = join(electrobunDir, 'src', 'cli', 'index.ts');
|
|
||||||
|
|
||||||
if (existsSync(patchedCliPath)) {
|
|
||||||
// farmcontrol-use-patched-cli: bundled electrobun.exe cannot resolve project rcedit
|
|
||||||
const bunBinary = process.env.BUN_INSTALL
|
|
||||||
? join(process.env.BUN_INSTALL, 'bin', 'bun' + binExt)
|
|
||||||
: 'bun';
|
|
||||||
const child = spawn(bunBinary, [patchedCliPath, ...args], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: process.env,
|
|
||||||
shell: platform === 'win',
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
process.exit(code || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (error) => {
|
|
||||||
console.error('Failed to start electrobun patched CLI:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cliPath = await ensureCliBinary();
|
|
||||||
|
|
||||||
// Replace this process with the actual CLI
|
|
||||||
const child = spawn(cliPath, args, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: process.cwd()
|
|
||||||
});`
|
|
||||||
)
|
|
||||||
writeFileSync(electrobunCjsPath, electrobunCjs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rceditSrc = path.join(rootDir, 'node_modules/rcedit')
|
|
||||||
const rceditDest = path.join(electrobunDir, 'node_modules/rcedit')
|
|
||||||
if (existsSync(rceditSrc)) {
|
|
||||||
mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true })
|
|
||||||
cpSync(rceditSrc, rceditDest, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchNativeWrapperPath(nativeTsPath) {
|
|
||||||
if (!existsSync(nativeTsPath)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = readFileSync(nativeTsPath, 'utf8')
|
|
||||||
|
|
||||||
if (source.includes('function resolveNativeWrapperPath()')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!source.includes('existsSync')) {
|
|
||||||
if (source.includes('import { dirname, join } from "path";')) {
|
|
||||||
source = source.replace(
|
|
||||||
'import { dirname, join } from "path";',
|
|
||||||
'import { existsSync } from "fs";\nimport { dirname, join } from "path";'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
source = source.replace(
|
|
||||||
'import { join } from "path";',
|
|
||||||
'import { existsSync } from "fs";\nimport { dirname, join } from "path";'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (!source.includes('dirname')) {
|
|
||||||
source = source.replace(
|
|
||||||
'import { join } from "path";',
|
|
||||||
'import { dirname, join } from "path";'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const helper = `
|
|
||||||
function resolveNativeWrapperPath() {
|
|
||||||
\tconst fileName = \`libNativeWrapper.\${suffix}\`;
|
|
||||||
\tconst candidates = [
|
|
||||||
\t\tjoin(dirname(process.execPath), fileName),
|
|
||||||
\t\tjoin(process.cwd(), fileName),
|
|
||||||
\t];
|
|
||||||
\tfor (const candidate of candidates) {
|
|
||||||
\t\tif (existsSync(candidate)) {
|
|
||||||
\t\t\treturn candidate;
|
|
||||||
\t\t}
|
|
||||||
\t}
|
|
||||||
\treturn candidates[0]!;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
source = source.replace(
|
|
||||||
'export const native = (() => {',
|
|
||||||
`${helper}\nexport const native = (() => {`
|
|
||||||
)
|
|
||||||
|
|
||||||
source = source.replace(
|
|
||||||
/const nativeWrapperPath = join\((?:dirname\(process\.execPath\)|process\.cwd\(\)), `libNativeWrapper\.\$\{suffix\}`\);/,
|
|
||||||
'const nativeWrapperPath = resolveNativeWrapperPath();'
|
|
||||||
)
|
|
||||||
|
|
||||||
writeFileSync(nativeTsPath, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const distDir of ['dist', 'dist-macos-arm64', 'dist-win-x64']) {
|
|
||||||
patchNativeWrapperPath(
|
|
||||||
path.join(electrobunDir, distDir, 'api/bun/proc/native.ts')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched')
|
|
||||||
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'patch-electrobun-src: electrobun src/shared ready for cross-arch builds'
|
|
||||||
)
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { existsSync, renameSync } from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
export const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe'
|
|
||||||
const SOURCE_LAUNCHER_EXE = 'launcher.exe'
|
|
||||||
|
|
||||||
export function patchWindowsBinaries(appDir) {
|
|
||||||
const binDir = path.join(appDir, 'bin')
|
|
||||||
if (!existsSync(binDir)) {
|
|
||||||
throw new Error(`patch-windows-binaries: bin directory not found at ${binDir}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourcePath = path.join(binDir, SOURCE_LAUNCHER_EXE)
|
|
||||||
const destPath = path.join(binDir, WINDOWS_LAUNCHER_EXE)
|
|
||||||
|
|
||||||
if (existsSync(destPath) && !existsSync(sourcePath)) {
|
|
||||||
console.log(`patch-windows-binaries: ${WINDOWS_LAUNCHER_EXE} already present`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(sourcePath)) {
|
|
||||||
console.warn(`patch-windows-binaries: skipping missing ${sourcePath}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(destPath)) {
|
|
||||||
renameSync(destPath, `${destPath}.old`)
|
|
||||||
}
|
|
||||||
|
|
||||||
renameSync(sourcePath, destPath)
|
|
||||||
console.log(
|
|
||||||
`patch-windows-binaries: renamed ${SOURCE_LAUNCHER_EXE} -> ${WINDOWS_LAUNCHER_EXE}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
||||||
const appDirArg = process.argv[2]
|
|
||||||
if (!appDirArg) {
|
|
||||||
console.error('Usage: bun scripts/patch-windows-binaries.mjs <app-dir>')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
patchWindowsBinaries(path.resolve(appDirArg))
|
|
||||||
}
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
|
|
||||||
|
|
||||||
const requiredFiles = [
|
|
||||||
"src/bun/index.js",
|
|
||||||
"electrobun.config.ts",
|
|
||||||
"package.json",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const relativePath of requiredFiles) {
|
|
||||||
const filePath = path.join(rootDir, relativePath);
|
|
||||||
if (!existsSync(filePath)) {
|
|
||||||
console.error(`pre-build: required file not found: ${relativePath}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildEnv === "stable" && process.env.ELECTROBUN_OS === "macos") {
|
|
||||||
const codesignVars = [
|
|
||||||
"ELECTROBUN_DEVELOPER_ID",
|
|
||||||
"ELECTROBUN_APPLEID",
|
|
||||||
"ELECTROBUN_APPLEIDPASS",
|
|
||||||
"ELECTROBUN_TEAMID",
|
|
||||||
];
|
|
||||||
const missing = codesignVars.filter((name) => !process.env[name]);
|
|
||||||
|
|
||||||
if (missing.length > 0) {
|
|
||||||
console.warn(
|
|
||||||
`pre-build: stable macOS build without code signing credentials (${missing.join(", ")}); Electrobun will skip signing.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildMacosEffects = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
|
|
||||||
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (buildMacosEffects.status !== 0) {
|
|
||||||
process.exit(buildMacosEffects.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
const { validateMacosEffectsDylib, outFile } = await import(
|
|
||||||
pathToFileURL(
|
|
||||||
path.join(rootDir, "scripts/build-macos-effects.mjs"),
|
|
||||||
).href
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validateMacosEffectsDylib({ dylibPath: outFile })) {
|
|
||||||
console.error(
|
|
||||||
"pre-build: macOS window effects dylib is missing or invalid; blur and traffic lights will not work in packaged builds.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredIconPaths = [
|
|
||||||
path.join(rootDir, "assets/icon.ico"),
|
|
||||||
path.join(rootDir, "assets/icon.png"),
|
|
||||||
path.join(rootDir, "assets/icon.iconset/icon_256x256.png"),
|
|
||||||
path.join(rootDir, "assets/installer.ico"),
|
|
||||||
path.join(rootDir, "assets/uninstaller.ico"),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (buildEnv === "stable") {
|
|
||||||
const prepareIcons = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/generate-app-icons.mjs")],
|
|
||||||
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (prepareIcons.status !== 0) {
|
|
||||||
process.exit(prepareIcons.status ?? 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingIcons = requiredIconPaths.filter((filePath) => !existsSync(filePath));
|
|
||||||
if (missingIcons.length > 0) {
|
|
||||||
console.error(
|
|
||||||
`pre-build: missing generated icons:\n${missingIcons
|
|
||||||
.map((filePath) => ` - ${path.relative(rootDir, filePath)}`)
|
|
||||||
.join("\n")}\nRun \`bun run generate-app-icons\` to create them.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeBuildInfo = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/write-build-info.mjs")],
|
|
||||||
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (writeBuildInfo.status !== 0) {
|
|
||||||
process.exit(writeBuildInfo.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildEnv === "dev") {
|
|
||||||
console.log(
|
|
||||||
"pre-build: dev environment — skipping production renderer build (use dev:renderer for Vite)",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Electrobun still copies dist/mainview into the app bundle; provide a stub so
|
|
||||||
// dev builds don't warn when Vite serves the renderer instead.
|
|
||||||
const stubDir = path.join(rootDir, "dist/mainview");
|
|
||||||
mkdirSync(stubDir, { recursive: true });
|
|
||||||
writeFileSync(
|
|
||||||
path.join(stubDir, "index.html"),
|
|
||||||
"<!doctype html><html><body>Dev mode — start Vite with <code>bun run dev:renderer</code>.</body></html>",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`pre-build: validation passed (${buildEnv})`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildRenderer = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/build-renderer.mjs")],
|
|
||||||
{
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
NODE_ENV: "production",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (buildRenderer.status !== 0) {
|
|
||||||
process.exit(buildRenderer.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rendererIndex = path.join(rootDir, "dist/mainview/index.html");
|
|
||||||
if (!existsSync(rendererIndex)) {
|
|
||||||
console.error(`pre-build: renderer output not found: dist/mainview/index.html`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`pre-build: validation and renderer build passed (${buildEnv})`);
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { Jimp } from "jimp";
|
|
||||||
import pngToIco from "png-to-ico";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
|
|
||||||
const MAC_ICONSET_ENTRIES = [
|
|
||||||
{ name: "icon_16x16.png", size: 16 },
|
|
||||||
{ name: "icon_16x16@2x.png", size: 32 },
|
|
||||||
{ name: "icon_32x32.png", size: 32 },
|
|
||||||
{ name: "icon_32x32@2x.png", size: 64 },
|
|
||||||
{ name: "icon_128x128.png", size: 128 },
|
|
||||||
{ name: "icon_128x128@2x.png", size: 256 },
|
|
||||||
{ name: "icon_256x256.png", size: 256 },
|
|
||||||
{ name: "icon_256x256@2x.png", size: 512 },
|
|
||||||
{ name: "icon_512x512.png", size: 512 },
|
|
||||||
{ name: "icon_512x512@2x.png", size: 1024 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const WIN_ICO_SIZES = [16, 32, 48, 256];
|
|
||||||
const LINUX_ICON_SIZE = 256;
|
|
||||||
|
|
||||||
function resolveSourcePath(arg) {
|
|
||||||
if (!arg) {
|
|
||||||
return path.join(rootDir, "assets/farmcontrolhosticon.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = path.isAbsolute(arg) ? arg : path.resolve(rootDir, arg);
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resizePng(sourceImage, size) {
|
|
||||||
return sourceImage
|
|
||||||
.clone()
|
|
||||||
.resize({ w: size, h: size })
|
|
||||||
.getBuffer("image/png");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writePng(filePath, sourceImage, size) {
|
|
||||||
const png = await resizePng(sourceImage, size);
|
|
||||||
await Bun.write(filePath, png);
|
|
||||||
return png;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const sourceArg = argv.find((arg) => !arg.startsWith("-"));
|
|
||||||
const outDirArgIndex = argv.indexOf("--out-dir");
|
|
||||||
const outDir =
|
|
||||||
outDirArgIndex >= 0 ? argv[outDirArgIndex + 1] : path.join(rootDir, "assets");
|
|
||||||
|
|
||||||
return {
|
|
||||||
sourcePath: resolveSourcePath(sourceArg),
|
|
||||||
outDir: path.isAbsolute(outDir) ? outDir : path.resolve(rootDir, outDir),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { sourcePath, outDir } = parseArgs(process.argv.slice(2));
|
|
||||||
|
|
||||||
if (!existsSync(sourcePath)) {
|
|
||||||
console.error(`prepare-app-icons: source image not found: ${sourcePath}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceImage = await Jimp.read(sourcePath);
|
|
||||||
if (sourceImage.width < 256 || sourceImage.height < 256) {
|
|
||||||
console.warn(
|
|
||||||
`prepare-app-icons: source image is ${sourceImage.width}x${sourceImage.height}; Electrobun recommends at least 256x256`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
|
||||||
|
|
||||||
const iconsetDir = path.join(outDir, "icon.iconset");
|
|
||||||
rmSync(iconsetDir, { recursive: true, force: true });
|
|
||||||
mkdirSync(iconsetDir, { recursive: true });
|
|
||||||
|
|
||||||
for (const entry of MAC_ICONSET_ENTRIES) {
|
|
||||||
const iconPath = path.join(iconsetDir, entry.name);
|
|
||||||
await writePng(iconPath, sourceImage, entry.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
const icoPngs = [];
|
|
||||||
for (const size of WIN_ICO_SIZES) {
|
|
||||||
icoPngs.push(await resizePng(sourceImage, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
const icoPath = path.join(outDir, "icon.ico");
|
|
||||||
await Bun.write(icoPath, await pngToIco(icoPngs));
|
|
||||||
|
|
||||||
const linuxIconPath = path.join(outDir, "icon.png");
|
|
||||||
await writePng(linuxIconPath, sourceImage, LINUX_ICON_SIZE);
|
|
||||||
|
|
||||||
console.log(`prepare-app-icons: wrote ${iconsetDir}`);
|
|
||||||
console.log(`prepare-app-icons: wrote ${icoPath}`);
|
|
||||||
console.log(`prepare-app-icons: wrote ${linuxIconPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(`prepare-app-icons: ${error.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { Jimp } from 'jimp'
|
|
||||||
import pngToIco from 'png-to-ico'
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
const INSTALLER_SOURCE = path.join(
|
|
||||||
rootDir,
|
|
||||||
'assets/logos/farmcontrolinstaller.png'
|
|
||||||
)
|
|
||||||
const UNINSTALLER_SOURCE = path.join(
|
|
||||||
rootDir,
|
|
||||||
'assets/logos/farmcontroluninstaller.png'
|
|
||||||
)
|
|
||||||
|
|
||||||
const MAC_ICONSET_ENTRIES = [
|
|
||||||
{ name: 'icon_16x16.png', size: 16 },
|
|
||||||
{ name: 'icon_16x16@2x.png', size: 32 },
|
|
||||||
{ name: 'icon_32x32.png', size: 32 },
|
|
||||||
{ name: 'icon_32x32@2x.png', size: 64 },
|
|
||||||
{ name: 'icon_128x128.png', size: 128 },
|
|
||||||
{ name: 'icon_128x128@2x.png', size: 256 },
|
|
||||||
{ name: 'icon_256x256.png', size: 256 },
|
|
||||||
{ name: 'icon_256x256@2x.png', size: 512 },
|
|
||||||
{ name: 'icon_512x512.png', size: 512 },
|
|
||||||
{ name: 'icon_512x512@2x.png', size: 1024 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const WIN_ICO_SIZES = [16, 32, 48, 256]
|
|
||||||
|
|
||||||
async function resizePng(sourceImage, size) {
|
|
||||||
return sourceImage
|
|
||||||
.clone()
|
|
||||||
.resize({ w: size, h: size })
|
|
||||||
.getBuffer('image/png')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writePng(filePath, sourceImage, size) {
|
|
||||||
const png = await resizePng(sourceImage, size)
|
|
||||||
await Bun.write(filePath, png)
|
|
||||||
return png
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeWindowsIco(sourceImage, icoPath) {
|
|
||||||
const icoPngs = []
|
|
||||||
for (const size of WIN_ICO_SIZES) {
|
|
||||||
icoPngs.push(await resizePng(sourceImage, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bun.write(icoPath, await pngToIco(icoPngs))
|
|
||||||
console.log(`prepare-installer-icons: wrote ${icoPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
if (!existsSync(INSTALLER_SOURCE)) {
|
|
||||||
console.error(
|
|
||||||
`prepare-installer-icons: source image not found: ${INSTALLER_SOURCE}`
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(UNINSTALLER_SOURCE)) {
|
|
||||||
console.error(
|
|
||||||
`prepare-installer-icons: source image not found: ${UNINSTALLER_SOURCE}`
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const installerImage = await Jimp.read(INSTALLER_SOURCE)
|
|
||||||
if (installerImage.width < 256 || installerImage.height < 256) {
|
|
||||||
console.warn(
|
|
||||||
`prepare-installer-icons: installer source is ${installerImage.width}x${installerImage.height}; recommend at least 256x256`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uninstallerImage = await Jimp.read(UNINSTALLER_SOURCE)
|
|
||||||
if (uninstallerImage.width < 256 || uninstallerImage.height < 256) {
|
|
||||||
console.warn(
|
|
||||||
`prepare-installer-icons: uninstaller source is ${uninstallerImage.width}x${uninstallerImage.height}; recommend at least 256x256`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsDir = path.join(rootDir, 'assets')
|
|
||||||
mkdirSync(assetsDir, { recursive: true })
|
|
||||||
|
|
||||||
const iconsetDir = path.join(assetsDir, 'installer.iconset')
|
|
||||||
rmSync(iconsetDir, { recursive: true, force: true })
|
|
||||||
mkdirSync(iconsetDir, { recursive: true })
|
|
||||||
|
|
||||||
for (const entry of MAC_ICONSET_ENTRIES) {
|
|
||||||
await writePng(
|
|
||||||
path.join(iconsetDir, entry.name),
|
|
||||||
installerImage,
|
|
||||||
entry.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`prepare-installer-icons: wrote ${iconsetDir}`)
|
|
||||||
await writeWindowsIco(installerImage, path.join(assetsDir, 'installer.ico'))
|
|
||||||
await writeWindowsIco(
|
|
||||||
uninstallerImage,
|
|
||||||
path.join(assetsDir, 'uninstaller.ico')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(`prepare-installer-icons: ${error.message}`)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
export function getReleaseVersion(packageJson) {
|
|
||||||
return packageJson.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReleaseArch(platformArch = process.arch) {
|
|
||||||
if (platformArch === "arm64" || platformArch === "aarch64") {
|
|
||||||
return "arm64";
|
|
||||||
}
|
|
||||||
if (platformArch === "x64" || platformArch === "amd64") {
|
|
||||||
return "x64";
|
|
||||||
}
|
|
||||||
return platformArch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBundleCefEnabled(
|
|
||||||
value = process.env.ELECTROBUN_BUNDLE_CEF,
|
|
||||||
) {
|
|
||||||
return ["1", "true", "yes"].includes(String(value || "").toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReleaseArtifactName(version, arch, ext, options = {}) {
|
|
||||||
const cef =
|
|
||||||
typeof options === "boolean"
|
|
||||||
? options
|
|
||||||
: Boolean(options.cef ?? isBundleCefEnabled());
|
|
||||||
const cefSuffix = cef ? "-cef" : "";
|
|
||||||
return `farmcontrol-${version}-${arch}${cefSuffix}.${ext}`;
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
||||||
import { getReleaseArch, isBundleCefEnabled } from "./release-artifact-utils.mjs";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const electrobunCli = path.join(
|
|
||||||
rootDir,
|
|
||||||
"node_modules/electrobun/src/cli/index.ts",
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "stable";
|
|
||||||
const targetArch = getReleaseArch(
|
|
||||||
process.env.ELECTROBUN_TARGET_ARCH ||
|
|
||||||
process.env.ELECTROBUN_FORCE_ARCH ||
|
|
||||||
process.arch,
|
|
||||||
);
|
|
||||||
const bundleCef = isBundleCefEnabled();
|
|
||||||
|
|
||||||
const patchResult = spawnSync("bun", [path.join(rootDir, "scripts/patch-electrobun-src.mjs")], {
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: process.env,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (patchResult.status !== 0) {
|
|
||||||
process.exit(patchResult.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const macosEffectsResult = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
|
|
||||||
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (macosEffectsResult.status !== 0) {
|
|
||||||
process.exit(macosEffectsResult.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
const { validateMacosEffectsDylib, outFile } = await import(
|
|
||||||
pathToFileURL(
|
|
||||||
path.join(rootDir, "scripts/build-macos-effects.mjs"),
|
|
||||||
).href
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validateMacosEffectsDylib({ dylibPath: outFile })) {
|
|
||||||
console.error(
|
|
||||||
"run-electrobun-build: macOS window effects dylib is missing or invalid.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureCoreEnv = {
|
|
||||||
...process.env,
|
|
||||||
ELECTROBUN_TARGET_ARCH: targetArch,
|
|
||||||
ELECTROBUN_FORCE_ARCH: targetArch,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureCoreResult = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[path.join(rootDir, "scripts/ensure-electrobun-core.mjs")],
|
|
||||||
{
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env: ensureCoreEnv,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ensureCoreResult.status !== 0) {
|
|
||||||
process.exit(ensureCoreResult.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
ELECTROBUN_BUILD_ENV: buildEnv,
|
|
||||||
ELECTROBUN_OS: "macos",
|
|
||||||
ELECTROBUN_ARCH: targetArch,
|
|
||||||
ELECTROBUN_FORCE_ARCH: targetArch,
|
|
||||||
ELECTROBUN_TARGET_ARCH: targetArch,
|
|
||||||
NODE_ENV: process.env.NODE_ENV || "production",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`run-electrobun-build: env=${buildEnv} os=macos arch=${targetArch} cef=${bundleCef} (host ${getReleaseArch(process.arch)})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = spawnSync(
|
|
||||||
"bun",
|
|
||||||
[electrobunCli, "build", `--env=${buildEnv}`],
|
|
||||||
{
|
|
||||||
cwd: rootDir,
|
|
||||||
stdio: "inherit",
|
|
||||||
env,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { cpSync, mkdirSync, rmSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const buildDir = path.join(rootDir, "build");
|
|
||||||
const viewDir = path.join(rootDir, "dist", "mainview");
|
|
||||||
|
|
||||||
rmSync(viewDir, { recursive: true, force: true });
|
|
||||||
mkdirSync(viewDir, { recursive: true });
|
|
||||||
cpSync(buildDir, viewDir, { recursive: true });
|
|
||||||
|
|
||||||
console.log(`Synced renderer build to ${viewDir}`);
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const buildNumber =
|
|
||||||
process.env.BUILD_NUMBER || process.env.VITE_BUILD_NUMBER || "dev";
|
|
||||||
|
|
||||||
const buildInfoPath = path.join(rootDir, "src/buildInfo.json");
|
|
||||||
|
|
||||||
await Bun.write(
|
|
||||||
buildInfoPath,
|
|
||||||
`${JSON.stringify({ buildNumber }, null, 2)}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`write-build-info: ${buildInfoPath} (buildNumber=${buildNumber})`);
|
|
||||||
32
src/App.jsx
@ -6,7 +6,6 @@ import {
|
|||||||
Navigate
|
Navigate
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { App, ConfigProvider } from 'antd'
|
import { App, ConfigProvider } from 'antd'
|
||||||
import { lazy } from 'react'
|
|
||||||
|
|
||||||
import Dashboard from './components/Dashboard/Dashboard.jsx'
|
import Dashboard from './components/Dashboard/Dashboard.jsx'
|
||||||
import PrivateRoute from './components/PrivateRoute'
|
import PrivateRoute from './components/PrivateRoute'
|
||||||
@ -18,7 +17,6 @@ import {
|
|||||||
ElectronSpotlightContentPage
|
ElectronSpotlightContentPage
|
||||||
} from './components/Dashboard/context/SpotlightContext.jsx'
|
} from './components/Dashboard/context/SpotlightContext.jsx'
|
||||||
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
|
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
|
||||||
import { ActionsProvider } from './components/Dashboard/context/ActionsContext.jsx'
|
|
||||||
import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx'
|
import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -30,16 +28,9 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
|
|||||||
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
|
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
|
||||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
||||||
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||||
import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx'
|
|
||||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||||
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
||||||
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
|
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
|
||||||
import AuthLaunch from './components/App/AppLaunch.jsx'
|
|
||||||
import { TableStateProvider } from './components/Dashboard/context/TableStateContext.jsx'
|
|
||||||
const SlicerIntegration = lazy(
|
|
||||||
() =>
|
|
||||||
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
|
|
||||||
)
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProductionRoutes,
|
ProductionRoutes,
|
||||||
@ -47,8 +38,7 @@ import {
|
|||||||
FinanceRoutes,
|
FinanceRoutes,
|
||||||
SalesRoutes,
|
SalesRoutes,
|
||||||
ManagementRoutes,
|
ManagementRoutes,
|
||||||
DeveloperRoutes,
|
DeveloperRoutes
|
||||||
ModelRoutes
|
|
||||||
} from './routes'
|
} from './routes'
|
||||||
|
|
||||||
const getRouter = () => {
|
const getRouter = () => {
|
||||||
@ -85,17 +75,10 @@ const AppContent = () => {
|
|||||||
<PrintServerProvider>
|
<PrintServerProvider>
|
||||||
<ApiServerProvider>
|
<ApiServerProvider>
|
||||||
<MessageProvider>
|
<MessageProvider>
|
||||||
<TableStateProvider>
|
|
||||||
<AppUpdateProvider>
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<SpotlightProvider>
|
<SpotlightProvider>
|
||||||
<ActionsModalProvider>
|
<ActionsModalProvider>
|
||||||
<ActionsProvider>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
|
||||||
path='/applaunch'
|
|
||||||
element={<AuthLaunch />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/dashboard/electron/spotlightcontent'
|
path='/dashboard/electron/spotlightcontent'
|
||||||
element={
|
element={
|
||||||
@ -131,17 +114,10 @@ const AppContent = () => {
|
|||||||
path='/email/notification'
|
path='/email/notification'
|
||||||
element={<EmailNotificationTemplate />}
|
element={<EmailNotificationTemplate />}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/slicer'
|
|
||||||
element={<SlicerIntegration />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path='/dashboard'
|
path='/dashboard'
|
||||||
element={
|
element={
|
||||||
<PrivateRoute
|
<PrivateRoute component={() => <Dashboard />} />
|
||||||
component={() => <Dashboard />}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{ProductionRoutes}
|
{ProductionRoutes}
|
||||||
@ -149,7 +125,6 @@ const AppContent = () => {
|
|||||||
{FinanceRoutes}
|
{FinanceRoutes}
|
||||||
{SalesRoutes}
|
{SalesRoutes}
|
||||||
{ManagementRoutes}
|
{ManagementRoutes}
|
||||||
{ModelRoutes}
|
|
||||||
{DeveloperRoutes}
|
{DeveloperRoutes}
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
@ -162,12 +137,9 @@ const AppContent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</ActionsProvider>
|
|
||||||
</ActionsModalProvider>
|
</ActionsModalProvider>
|
||||||
</SpotlightProvider>
|
</SpotlightProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</AppUpdateProvider>
|
|
||||||
</TableStateProvider>
|
|
||||||
</MessageProvider>
|
</MessageProvider>
|
||||||
</ApiServerProvider>
|
</ApiServerProvider>
|
||||||
</PrintServerProvider>
|
</PrintServerProvider>
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"buildNumber": "dev"
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { spawn } from 'node:child_process'
|
|
||||||
import { existsSync } from 'node:fs'
|
|
||||||
import { dirname, join } from 'node:path'
|
|
||||||
import {
|
|
||||||
buildDeeplinkPayload,
|
|
||||||
findProtocolUrl,
|
|
||||||
forwardDeeplinkToRunningInstance,
|
|
||||||
writeDeeplinkSignal
|
|
||||||
} from '../desktop/deeplink-ipc.js'
|
|
||||||
|
|
||||||
const url = findProtocolUrl(process.argv)
|
|
||||||
|
|
||||||
console.log('[deeplink] argv:', process.argv)
|
|
||||||
console.log('[deeplink] url:', url ?? null)
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = buildDeeplinkPayload(url, process.argv)
|
|
||||||
const forwarded = await forwardDeeplinkToRunningInstance(payload)
|
|
||||||
|
|
||||||
if (forwarded) {
|
|
||||||
console.log('[deeplink] forwarded to running Farm Control instance')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[deeplink] no running instance found, launching Farm Control')
|
|
||||||
|
|
||||||
writeDeeplinkSignal(payload)
|
|
||||||
|
|
||||||
const binDir = dirname(process.execPath)
|
|
||||||
const launcherPath = ['FarmControl.exe', 'launcher.exe']
|
|
||||||
.map((name) => join(binDir, name))
|
|
||||||
.find((candidate) => existsSync(candidate))
|
|
||||||
|
|
||||||
if (!launcherPath) {
|
|
||||||
console.error('[deeplink] FarmControl.exe not found in', binDir)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const child = spawn(launcherPath, [], {
|
|
||||||
cwd: dirname(launcherPath),
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true
|
|
||||||
})
|
|
||||||
|
|
||||||
child.unref()
|
|
||||||
process.exit(0)
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { ensureWindowsWorkingDirectory } from '../desktop/windows-app-paths.js'
|
|
||||||
import {
|
|
||||||
captureLaunchUrl,
|
|
||||||
closeSingleInstanceServer,
|
|
||||||
ensureSingleInstanceLock
|
|
||||||
} from '../desktop/single-instance.js'
|
|
||||||
|
|
||||||
ensureWindowsWorkingDirectory()
|
|
||||||
|
|
||||||
const launchUrl = captureLaunchUrl()
|
|
||||||
const gotSingleInstanceLock = await ensureSingleInstanceLock({ launchUrl })
|
|
||||||
|
|
||||||
if (!gotSingleInstanceLock) {
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { createAppRpc } = await import('../desktop/rpc.js')
|
|
||||||
const {
|
|
||||||
registerGlobalShortcuts,
|
|
||||||
unregisterGlobalShortcuts
|
|
||||||
} = await import('../desktop/spotlight.js')
|
|
||||||
const {
|
|
||||||
createMainWindow,
|
|
||||||
handleDeepLinkFromArgv,
|
|
||||||
setupDevAuthServer,
|
|
||||||
setupNavigationGestures,
|
|
||||||
setupWindowsDeepLinkHandling
|
|
||||||
} = await import('../desktop/window.js')
|
|
||||||
|
|
||||||
setupWindowsDeepLinkHandling()
|
|
||||||
|
|
||||||
const rpc = createAppRpc()
|
|
||||||
const mainWindow = await createMainWindow(rpc)
|
|
||||||
|
|
||||||
setupNavigationGestures(mainWindow)
|
|
||||||
registerGlobalShortcuts(rpc)
|
|
||||||
setupDevAuthServer()
|
|
||||||
handleDeepLinkFromArgv(launchUrl)
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
unregisterGlobalShortcuts()
|
|
||||||
closeSingleInstanceServer()
|
|
||||||
})
|
|
||||||