fix(electron): added macos file associations
This commit is contained in:
428
assets/opat/fourdst_opat_icon.svg
Normal file
428
assets/opat/fourdst_opat_icon.svg
Normal file
@@ -0,0 +1,428 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="300mm"
|
||||
height="300mm"
|
||||
viewBox="0 0 300 300"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="icon2048.png"
|
||||
inkscape:export-xdpi="173.39734"
|
||||
inkscape:export-ydpi="173.39734"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
sodipodi:docname="fourdst_opat_icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5446877"
|
||||
inkscape:cx="398.39343"
|
||||
inkscape:cy="492.94302"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="968"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
id="linearGradient120"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#53ffc0;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop119" />
|
||||
<stop
|
||||
style="stop-color:#6bffc0;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop120" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient109"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#6bffc0;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop109" />
|
||||
<stop
|
||||
style="stop-color:#6bffc0;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop110" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient74"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#ffff74;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop73" />
|
||||
<stop
|
||||
style="stop-color:#ffff74;stop-opacity:0;"
|
||||
offset="0.67299581"
|
||||
id="stop74" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient60"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#ffff74;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop61" />
|
||||
<stop
|
||||
style="stop-color:#ffff74;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop62" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient6">
|
||||
<stop
|
||||
style="stop-color:#eeeeee;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop9" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop10" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient6-1"
|
||||
inkscape:label="other">
|
||||
<stop
|
||||
style="stop-color:#e1ddca;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6-1"
|
||||
id="linearGradient7"
|
||||
x1="42.292004"
|
||||
y1="73.68721"
|
||||
x2="169.77107"
|
||||
y2="225.61084"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(45.566128)" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath13">
|
||||
<rect
|
||||
style="fill:#2a676d;fill-opacity:1;stroke:none;stroke-width:2.68946;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect13"
|
||||
width="128.54131"
|
||||
height="128.54131"
|
||||
x="86.50499"
|
||||
y="62.050838"
|
||||
ry="32.050087" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient85"
|
||||
x1="166.98118"
|
||||
y1="169.43259"
|
||||
x2="231.79551"
|
||||
y2="169.43259"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient86"
|
||||
x1="145.93909"
|
||||
y1="160.81119"
|
||||
x2="225.07373"
|
||||
y2="160.81119"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient87"
|
||||
x1="126.65051"
|
||||
y1="146.8562"
|
||||
x2="229.01912"
|
||||
y2="146.8562"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient88"
|
||||
x1="104.14719"
|
||||
y1="137.13885"
|
||||
x2="224.19699"
|
||||
y2="137.13885"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient89"
|
||||
x1="89.534627"
|
||||
y1="127.20231"
|
||||
x2="216.59845"
|
||||
y2="127.20231"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient90"
|
||||
x1="79.89034"
|
||||
y1="116.38902"
|
||||
x2="201.69364"
|
||||
y2="116.38902"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient91"
|
||||
x1="70.246052"
|
||||
y1="105.86798"
|
||||
x2="184.45082"
|
||||
y2="105.86798"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient92"
|
||||
x1="72.584061"
|
||||
y1="92.497486"
|
||||
x2="166.18513"
|
||||
y2="92.497486"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient94"
|
||||
x1="205.95732"
|
||||
y1="53.032385"
|
||||
x2="246.9165"
|
||||
y2="53.032385"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient60"
|
||||
id="linearGradient95"
|
||||
x1="195.48145"
|
||||
y1="23.466793"
|
||||
x2="210.10456"
|
||||
y2="23.466793"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient74"
|
||||
id="linearGradient96"
|
||||
x1="195.4355"
|
||||
y1="41.890426"
|
||||
x2="235.15083"
|
||||
y2="41.890426"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient110"
|
||||
x1="74.045316"
|
||||
y1="81.172754"
|
||||
x2="145.28917"
|
||||
y2="81.172754"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient111"
|
||||
x1="166.98118"
|
||||
y1="169.43259"
|
||||
x2="231.79551"
|
||||
y2="169.43259"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient112"
|
||||
x1="145.93909"
|
||||
y1="160.81119"
|
||||
x2="225.07373"
|
||||
y2="160.81119"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient113"
|
||||
x1="126.65051"
|
||||
y1="146.8562"
|
||||
x2="229.01912"
|
||||
y2="146.8562"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient114"
|
||||
x1="104.14719"
|
||||
y1="137.13885"
|
||||
x2="224.19699"
|
||||
y2="137.13885"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient115"
|
||||
x1="89.534627"
|
||||
y1="127.20231"
|
||||
x2="216.59845"
|
||||
y2="127.20231"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient116"
|
||||
x1="79.89034"
|
||||
y1="116.38902"
|
||||
x2="201.69364"
|
||||
y2="116.38902"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient109"
|
||||
id="linearGradient117"
|
||||
x1="70.246052"
|
||||
y1="105.86798"
|
||||
x2="184.45082"
|
||||
y2="105.86798"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient120"
|
||||
id="linearGradient118"
|
||||
x1="72.584061"
|
||||
y1="92.497486"
|
||||
x2="166.18513"
|
||||
y2="92.497486"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:url(#linearGradient94);fill-opacity:1;stroke:none;stroke-width:0.309711;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
d="m 228.60488,59.536001 c -16.32692,-0.05564 -17.63801,-0.07865 -19.37835,-0.34004 -1.62726,-0.244409 -3.26921,-0.679037 -3.26921,-0.865367 0,-0.07898 0.0887,-0.06785 0.38266,0.04802 1.82755,0.720352 3.92686,0.889153 12.05023,0.96893 l 4.49655,0.04416 1.31933,-1.105725 c 0.72562,-0.60815 1.73346,-1.455743 2.23964,-1.883545 1.70861,-1.444059 2.83758,-2.395323 3.16605,-2.667706 0.17974,-0.149048 0.74098,-0.624354 1.24721,-1.056233 0.50624,-0.431882 1.31926,-1.123543 1.80674,-1.537025 1.66659,-1.413632 2.48178,-2.140715 2.48178,-2.213528 0,-0.04004 -0.52744,-0.612492 -1.1721,-1.272106 -0.64465,-0.659615 -1.14914,-1.199301 -1.12109,-1.199301 0.0673,0 14.06218,13.067313 14.06218,13.130138 0,0.01791 -0.44424,0.02635 -0.9872,0.01879 -0.54296,-0.0076 -8.33895,-0.03883 -17.32442,-0.06945 z"
|
||||
id="path9" />
|
||||
<path
|
||||
style="fill:url(#linearGradient95);fill-opacity:1;stroke:none;stroke-width:1.09929;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
d="M 195.48145,23.466792 V 10.379909 l 4.13836,3.947872 c 2.27609,2.17133 5.56629,5.308241 7.31155,6.970913 l 3.1732,3.023041 -7.31156,6.115971 -7.31155,6.115971 z"
|
||||
id="path8" />
|
||||
<path
|
||||
style="fill:url(#linearGradient96);fill-opacity:1;stroke:none;stroke-width:1.09929;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
d="m 208.41893,58.942628 c -4.18091,-0.756729 -8.56585,-3.864409 -10.65438,-7.550941 -1.6286,-2.874674 -2.06349,-4.854837 -2.20307,-10.030997 l -0.12598,-4.671973 7.32039,-6.134692 7.32039,-6.134694 9.67284,9.463639 c 5.32006,5.205 10.98449,10.686349 12.58762,12.180773 1.60313,1.494425 2.86866,2.801456 2.81228,2.904512 -0.0564,0.103057 -2.83214,2.483701 -6.16837,5.290322 l -6.06588,5.102945 -6.22776,-0.02479 c -3.90945,-0.01556 -6.98728,-0.162271 -8.26808,-0.394091 z"
|
||||
id="path7" />
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:url(#linearGradient7);stroke:#000000;stroke-width:1.05833;stroke-dasharray:none"
|
||||
d="M 73.519893,7.7113838 194.17488,7.9273849 208.07531,21.319453 c 0,0 17.49712,17.497087 24.08044,23.64844 l 17.06409,15.944416 0.216,210.913961 c 0.0112,10.94726 -8.81315,19.76042 -19.76042,19.76042 H 73.519893 c -10.947273,0 -19.760419,-8.81315 -19.760419,-19.76042 V 27.471804 c 0,-10.947273 8.813164,-19.7800184 19.760419,-19.7604202 z"
|
||||
sodipodi:nodetypes="sccccssssss" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 194.8207,8.7052203 c 0,0 0.10492,19.7156627 -0.0425,28.9586697 -0.0964,6.043642 -0.31445,12.15661 4.97306,17.437969 4.57274,4.567432 9.32915,4.928134 14.40725,4.951712 11.22158,0.0521 34.19919,0.100814 34.19919,0.100814 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cssscc" />
|
||||
<rect
|
||||
style="fill:#2a676d;fill-opacity:1;stroke:none;stroke-width:2.68947;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect7"
|
||||
width="128.54131"
|
||||
height="128.54131"
|
||||
x="86.559303"
|
||||
y="62.109989"
|
||||
ry="32.050087" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:8.81944px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#d35f5f;stroke:none;stroke-width:2.86782;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
x="30.913881"
|
||||
y="259.40781"
|
||||
id="text5"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5"
|
||||
style="stroke:none;stroke-width:2.86782"
|
||||
x="30.913881"
|
||||
y="259.40781" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:46.2556px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#d35f5f;stroke:none;stroke-width:5.01364;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
x="153.43289"
|
||||
y="252.49603"
|
||||
id="text6"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6"
|
||||
style="font-size:46.2556px;fill:#858585;fill-opacity:1;stroke-width:5.01364"
|
||||
x="153.43289"
|
||||
y="252.49603">OPAT</tspan></text>
|
||||
<g
|
||||
id="g13"
|
||||
clip-path="url(#clipPath13)">
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:url(#linearGradient110);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 75.108543,115.73145 144.22594,46.614058"
|
||||
id="path2" />
|
||||
<path
|
||||
style="fill:url(#linearGradient92);fill-opacity:1;stroke:url(#linearGradient118);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 73.647288,138.23479 165.1219,46.760182"
|
||||
id="path3" />
|
||||
<path
|
||||
style="fill:url(#linearGradient91);fill-opacity:1;stroke:url(#linearGradient117);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 71.309279,161.90714 183.38759,49.828819"
|
||||
id="path4" />
|
||||
<path
|
||||
style="fill:url(#linearGradient90);fill-opacity:1;stroke:url(#linearGradient116);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 80.953567,176.22744 200.63041,56.550595"
|
||||
id="path5" />
|
||||
<path
|
||||
style="fill:url(#linearGradient89);fill-opacity:1;stroke:url(#linearGradient115);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 90.597854,189.67099 215.53522,64.733627"
|
||||
id="path6" />
|
||||
<path
|
||||
style="fill:url(#linearGradient88);fill-opacity:1;stroke:url(#linearGradient114);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 105.21042,196.10052 223.13376,78.17718"
|
||||
id="path10" />
|
||||
<path
|
||||
style="fill:url(#linearGradient87);fill-opacity:1;stroke:url(#linearGradient113);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 127.71374,196.97727 227.95589,96.735127"
|
||||
id="path11" />
|
||||
<path
|
||||
style="fill:url(#linearGradient86);fill-opacity:1;stroke:url(#linearGradient112);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="M 147.00232,199.31528 224.0105,122.3071"
|
||||
id="path12" />
|
||||
<path
|
||||
style="fill:url(#linearGradient85);fill-opacity:1;stroke:url(#linearGradient111);stroke-width:1.50363;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:0.70523399;paint-order:stroke fill markers"
|
||||
d="m 168.04441,200.77653 62.68787,-62.68787"
|
||||
id="path13" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
256
electron/PACKAGING_SOLUTION_SUMMARY.md
Normal file
256
electron/PACKAGING_SOLUTION_SUMMARY.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# 4DSTAR Bundle Manager - Complete Packaging Solution
|
||||
|
||||
## 🎯 **Mission Accomplished**
|
||||
|
||||
This document summarizes the complete packaging and deployment solution for the 4DSTAR Electron app, addressing all original requirements and user feedback.
|
||||
|
||||
## ✅ **Problems Solved**
|
||||
|
||||
### 1. **JSON Parsing Errors (Original Issue)**
|
||||
- **Problem**: Backend outputting non-JSON text contaminating stdout
|
||||
- **Root Cause**: ABI detection using external `meson` command and print statements
|
||||
- **Solution**: Refactored platform detection with fallback, replaced print with logging
|
||||
- **Status**: ✅ **RESOLVED** - Clean JSON output guaranteed
|
||||
|
||||
### 2. **Runtime Dependencies (Self-Contained App)**
|
||||
- **Problem**: App required external dependencies (meson, Python modules)
|
||||
- **Solution**: Enhanced PyInstaller spec with comprehensive hidden imports
|
||||
- **Validation**: Integrated dependency validation scripts
|
||||
- **Status**: ✅ **RESOLVED** - Fully self-contained
|
||||
|
||||
### 3. **File Associations (macOS Integration)**
|
||||
- **Problem**: No file associations for .fbundle and .opat files
|
||||
- **Solution**: Complete Info.plist configuration with custom icons
|
||||
- **Status**: ✅ **RESOLVED** - Native macOS file handling
|
||||
|
||||
### 4. **Icon Generation (Professional Appearance)**
|
||||
- **Problem**: No custom icons for file types
|
||||
- **Solution**: Automated icon generation from SVG sources
|
||||
- **Status**: ✅ **RESOLVED** - Professional custom icons
|
||||
|
||||
### 5. **App Crashes on File Association (Timing Issue)**
|
||||
- **Problem**: "Cannot create BrowserWindow before app is ready"
|
||||
- **Solution**: Event queuing system for file open requests
|
||||
- **Status**: ✅ **RESOLVED** - Crash-free file associations
|
||||
|
||||
### 6. **Icon Refresh Issues (macOS Quirks)**
|
||||
- **Problem**: File icons don't update immediately after install
|
||||
- **Solution**: Automated post-install script + manual refresh tools
|
||||
- **Status**: ✅ **RESOLVED** - Automatic icon refresh
|
||||
|
||||
### 7. **Professional Installation Experience**
|
||||
- **Problem**: Basic .dmg installer with no guidance
|
||||
- **Solution**: Professional .pkg installer with dialogs and automation
|
||||
- **Status**: ✅ **RESOLVED** - Enterprise-grade installer
|
||||
|
||||
## 🏗️ **Architecture Overview**
|
||||
|
||||
### **Backend (Python)**
|
||||
```
|
||||
fourdst-backend (PyInstaller executable)
|
||||
├── All Python dependencies embedded
|
||||
├── No external meson dependency
|
||||
├── Clean JSON-only stdout
|
||||
├── Logging to stderr
|
||||
└── Fallback platform detection
|
||||
```
|
||||
|
||||
### **Frontend (Electron)**
|
||||
```
|
||||
4DSTAR Bundle Manager.app
|
||||
├── Self-contained Node.js modules
|
||||
├── File association handlers
|
||||
├── Icon generation system
|
||||
├── Dependency validation
|
||||
└── Runtime checking
|
||||
```
|
||||
|
||||
### **Installer (.pkg)**
|
||||
```
|
||||
Professional macOS installer
|
||||
├── Welcome dialog (dependency guidance)
|
||||
├── Automatic post-install script
|
||||
├── Launch Services refresh
|
||||
├── File association setup
|
||||
└── Conclusion dialog (next steps)
|
||||
```
|
||||
|
||||
## 📦 **Deliverables**
|
||||
|
||||
### **For End Users**
|
||||
1. **`4DSTAR Bundle Manager-1.0.0.pkg`** - Professional installer
|
||||
2. **`4DSTAR Bundle Manager-1.0.0.dmg`** - Traditional disk image
|
||||
3. **`4DSTAR Bundle Manager-1.0.0-mac.zip`** - Portable archive
|
||||
|
||||
### **For Developers**
|
||||
1. **Complete build system** with validation
|
||||
2. **Icon generation pipeline** from SVG sources
|
||||
3. **Dependency embedding** documentation
|
||||
4. **Testing and debugging** tools
|
||||
|
||||
## 🔧 **Technical Implementation**
|
||||
|
||||
### **File Association System**
|
||||
```javascript
|
||||
// Main Process (app-lifecycle.js)
|
||||
app.on('open-file', (event, filePath) => {
|
||||
if (!app.isReady()) {
|
||||
pendingFileToOpen = filePath; // Queue until ready
|
||||
return;
|
||||
}
|
||||
handleFileOpen(filePath);
|
||||
});
|
||||
|
||||
// Renderer Process (event-handlers.js)
|
||||
ipcRenderer.on('open-bundle-file', async (event, filePath) => {
|
||||
await bundleOperations.openBundleFromPath(filePath);
|
||||
});
|
||||
```
|
||||
|
||||
### **Icon Generation Pipeline**
|
||||
```bash
|
||||
# Automated during build
|
||||
npm run generate-icons
|
||||
|
||||
# Sources:
|
||||
assets/toolkit/appicon/toolkitIcon.svg → app-icon.icns
|
||||
assets/bundle/fourdst_bundle_icon.svg → fbundle-icon.icns
|
||||
assets/opat/fourdst_opat_icon.svg → opat-icon.icns
|
||||
```
|
||||
|
||||
### **Post-Install Automation**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# installer-scripts/postinstall
|
||||
lsregister -kill -r -domain local -domain system -domain user
|
||||
lsregister -f "/Applications/4DSTAR Bundle Manager.app"
|
||||
killall Finder
|
||||
```
|
||||
|
||||
## 🚀 **User Experience**
|
||||
|
||||
### **Installation Flow**
|
||||
1. **Download** `.pkg` installer
|
||||
2. **Welcome Dialog** explains dependencies
|
||||
3. **Standard Installation** to /Applications
|
||||
4. **Automatic Setup** runs post-install script
|
||||
5. **Conclusion Dialog** provides next steps
|
||||
6. **Immediate Functionality** - file associations work
|
||||
|
||||
### **Daily Usage**
|
||||
- ✅ Double-click `.fbundle` files → Opens in Bundle Manager
|
||||
- ✅ Double-click `.opat` files → Opens in OPAT Core section
|
||||
- ✅ Custom icons in Finder
|
||||
- ✅ Right-click → "Open with 4DSTAR Bundle Manager"
|
||||
- ✅ No external dependencies for basic usage
|
||||
|
||||
## 📊 **Validation Results**
|
||||
|
||||
### **Build Validation**
|
||||
```
|
||||
✅ fileStructure: PASS
|
||||
✅ nodeDependencies: PASS
|
||||
✅ electronBuild: PASS
|
||||
✅ pyinstallerSpec: PASS
|
||||
✅ pythonBackend: PASS
|
||||
🎉 All validations passed!
|
||||
```
|
||||
|
||||
### **Runtime Validation**
|
||||
- ✅ Backend executable found and functional
|
||||
- ✅ JSON output verified
|
||||
- ✅ File associations working
|
||||
- ✅ Icons displaying correctly
|
||||
- ✅ No external dependencies required
|
||||
|
||||
## 🛠️ **Development Workflow**
|
||||
|
||||
### **Building**
|
||||
```bash
|
||||
npm run build # Full build with all targets
|
||||
npm run generate-icons # Regenerate icons only
|
||||
npm run validate-deps # Validate dependencies
|
||||
npm run refresh-icons # Manual icon refresh
|
||||
```
|
||||
|
||||
### **Testing**
|
||||
```bash
|
||||
npm run check-runtime # Runtime dependency check
|
||||
node debug-packaged-app.js # Backend testing
|
||||
```
|
||||
|
||||
### **Distribution**
|
||||
1. Build with `npm run build`
|
||||
2. Test .pkg installer on clean system
|
||||
3. Verify file associations work immediately
|
||||
4. Distribute via preferred channel
|
||||
|
||||
## 📋 **Optional Dependencies**
|
||||
|
||||
### **For End Users (Basic Usage)**
|
||||
- **Required**: macOS 10.12+
|
||||
- **Optional**: None
|
||||
|
||||
### **For Developers (Plugin Building)**
|
||||
- **Docker Desktop**: Cross-platform builds
|
||||
- **Meson**: Native compilation
|
||||
- **Xcode CLI Tools**: C++ compilation
|
||||
|
||||
### **Clear Communication**
|
||||
The installer clearly explains:
|
||||
- What works without dependencies
|
||||
- What requires additional tools
|
||||
- How to install optional dependencies
|
||||
- Alternatives for each use case
|
||||
|
||||
## 🎉 **Success Metrics**
|
||||
|
||||
### **Technical Excellence**
|
||||
- ✅ Zero external runtime dependencies
|
||||
- ✅ Professional installer experience
|
||||
- ✅ Native macOS integration
|
||||
- ✅ Crash-free operation
|
||||
- ✅ Immediate functionality
|
||||
|
||||
### **User Experience**
|
||||
- ✅ One-click installation
|
||||
- ✅ Automatic file association setup
|
||||
- ✅ Clear dependency guidance
|
||||
- ✅ Professional appearance
|
||||
- ✅ Comprehensive documentation
|
||||
|
||||
### **Developer Experience**
|
||||
- ✅ Automated build pipeline
|
||||
- ✅ Comprehensive validation
|
||||
- ✅ Easy customization
|
||||
- ✅ Debugging tools
|
||||
- ✅ Clear documentation
|
||||
|
||||
## 🔮 **Future Enhancements**
|
||||
|
||||
### **Potential Improvements**
|
||||
1. **Code Signing**: Apple Developer ID for Gatekeeper compatibility
|
||||
2. **Notarization**: Apple notarization for enhanced security
|
||||
3. **Auto-Updates**: Electron auto-updater integration
|
||||
4. **Telemetry**: Usage analytics and crash reporting
|
||||
5. **Localization**: Multi-language installer support
|
||||
|
||||
### **Maintenance**
|
||||
- Monitor for macOS compatibility issues
|
||||
- Update dependencies regularly
|
||||
- Test on new macOS versions
|
||||
- Gather user feedback for improvements
|
||||
|
||||
## 📞 **Support**
|
||||
|
||||
### **For Users**
|
||||
- Installation issues: Check PKG_INSTALLER_GUIDE.md
|
||||
- File association problems: Run refresh-icons script
|
||||
- General usage: Application help documentation
|
||||
|
||||
### **For Developers**
|
||||
- Build issues: Check DEPENDENCY_EMBEDDING_SOLUTION.md
|
||||
- Packaging problems: Review validation scripts
|
||||
- Customization: Modify installer resources
|
||||
|
||||
This solution represents a **complete, professional packaging system** that transforms the 4DSTAR Bundle Manager from a development tool into a **production-ready application** with enterprise-grade installation and user experience.
|
||||
242
electron/PKG_INSTALLER_GUIDE.md
Normal file
242
electron/PKG_INSTALLER_GUIDE.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 4DSTAR Bundle Manager .pkg Installer Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The 4DSTAR Bundle Manager now includes a professional macOS `.pkg` installer that provides a seamless installation experience with automatic file association setup and dependency guidance.
|
||||
|
||||
## What's Included
|
||||
|
||||
### 📦 **Professional Installer Package**
|
||||
- Native macOS `.pkg` installer format
|
||||
- Automatic Launch Services refresh
|
||||
- File association setup
|
||||
- Custom icon registration
|
||||
- Dependency information dialogs
|
||||
|
||||
### 🔧 **Automatic Post-Install Setup**
|
||||
- Launch Services database refresh
|
||||
- File association registration
|
||||
- Icon cache clearing
|
||||
- Finder restart for immediate functionality
|
||||
|
||||
### 📋 **User Guidance**
|
||||
- Welcome dialog explaining system requirements
|
||||
- Clear distinction between basic and advanced usage
|
||||
- Optional dependency installation instructions
|
||||
- Conclusion dialog with next steps
|
||||
|
||||
## Installation Experience
|
||||
|
||||
### 1. **Welcome Screen**
|
||||
Users see a comprehensive welcome dialog that explains:
|
||||
- **Basic Usage**: No dependencies required for bundle management
|
||||
- **Advanced Usage**: Docker and Meson needed for plugin building
|
||||
- **System Requirements**: macOS 10.12+ minimum
|
||||
- **What's Included**: App, file associations, custom icons
|
||||
|
||||
### 2. **Standard macOS Installation**
|
||||
- License agreement (if configured)
|
||||
- Installation destination (defaults to /Applications)
|
||||
- Administrator password prompt
|
||||
- Installation progress
|
||||
|
||||
### 3. **Automatic Post-Install**
|
||||
The installer automatically:
|
||||
- Refreshes Launch Services database
|
||||
- Registers file associations for .fbundle and .opat files
|
||||
- Clears icon caches
|
||||
- Restarts Finder
|
||||
- Logs all operations to `/tmp/4dstar-postinstall.log`
|
||||
|
||||
### 4. **Conclusion Screen**
|
||||
Users see a success dialog with:
|
||||
- Installation confirmation
|
||||
- Getting started instructions
|
||||
- Optional dependency installation commands
|
||||
- Troubleshooting information
|
||||
|
||||
## File Associations
|
||||
|
||||
### Supported File Types
|
||||
- **`.fbundle`**: 4DSTAR Plugin Bundle Files
|
||||
- **`.opat`**: OPAT Data Files
|
||||
|
||||
### What Works After Installation
|
||||
- ✅ Double-click files to open in 4DSTAR Bundle Manager
|
||||
- ✅ Right-click → "Open with 4DSTAR Bundle Manager"
|
||||
- ✅ Custom file icons in Finder
|
||||
- ✅ Proper file type descriptions
|
||||
- ✅ Immediate functionality (no restart required)
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Required (Always)
|
||||
- macOS 10.12 or later
|
||||
- No additional dependencies for basic bundle management
|
||||
|
||||
### Optional (Advanced Features)
|
||||
- **Docker Desktop**: For cross-platform plugin builds
|
||||
- **Meson Build System**: For native plugin compilation
|
||||
- **Xcode Command Line Tools**: For C++ compilation
|
||||
|
||||
### Installation Commands (Optional)
|
||||
```bash
|
||||
# Install Xcode Command Line Tools
|
||||
xcode-select --install
|
||||
|
||||
# Install Meson via Homebrew
|
||||
brew install meson
|
||||
|
||||
# Download Docker Desktop
|
||||
# Visit: https://docker.com/products/docker-desktop
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### Package.json Configuration
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"mac": {
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] },
|
||||
{ "target": "pkg", "arch": ["x64", "arm64"] },
|
||||
{ "target": "zip", "arch": ["x64", "arm64"] }
|
||||
]
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": "installer-scripts",
|
||||
"welcome": "installer-resources/welcome.html",
|
||||
"conclusion": "installer-resources/conclusion.html",
|
||||
"installLocation": "/Applications",
|
||||
"mustClose": ["com.fourdst.bundlemanager"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Structure
|
||||
```
|
||||
electron/
|
||||
├── installer-scripts/
|
||||
│ └── postinstall # Post-install script
|
||||
├── installer-resources/
|
||||
│ ├── welcome.html # Welcome dialog
|
||||
│ └── conclusion.html # Conclusion dialog
|
||||
├── icons/
|
||||
│ ├── app-icon.icns # Main app icon
|
||||
│ ├── fbundle-icon.icns # .fbundle file icon
|
||||
│ └── opat-icon.icns # .opat file icon
|
||||
└── package.json # Build configuration
|
||||
```
|
||||
|
||||
## Building the Installer
|
||||
|
||||
### Generate All Installers
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Generate Only .pkg
|
||||
```bash
|
||||
npx electron-builder --mac pkg
|
||||
```
|
||||
|
||||
### Output Files
|
||||
- `dist/4DSTAR Bundle Manager-1.0.0.pkg` (x64)
|
||||
- `dist/4DSTAR Bundle Manager-1.0.0-arm64.pkg` (ARM64)
|
||||
- Plus traditional .dmg and .zip files
|
||||
|
||||
## Post-Install Script Details
|
||||
|
||||
### What It Does
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Reset Launch Services database
|
||||
lsregister -kill -r -domain local -domain system -domain user
|
||||
|
||||
# Register the app bundle
|
||||
lsregister -f "/Applications/4DSTAR Bundle Manager.app"
|
||||
|
||||
# Clear icon caches
|
||||
rm -rf ~/Library/Caches/com.apple.iconservices.store
|
||||
rm -rf /Library/Caches/com.apple.iconservices.store
|
||||
|
||||
# Restart Finder
|
||||
killall Finder
|
||||
```
|
||||
|
||||
### Logging
|
||||
All operations are logged to `/tmp/4dstar-postinstall.log` for troubleshooting.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### If File Associations Don't Work
|
||||
1. Check post-install log: `cat /tmp/4dstar-postinstall.log`
|
||||
2. Manually refresh: `npm run refresh-icons`
|
||||
3. Right-click file → Get Info → Change default app
|
||||
|
||||
### If Icons Don't Appear
|
||||
1. Wait 2-3 minutes for macOS to update
|
||||
2. Log out and back in
|
||||
3. Restart the Mac
|
||||
4. Check icon cache clearing in post-install log
|
||||
|
||||
### If Installation Fails
|
||||
1. Ensure you have administrator privileges
|
||||
2. Close any running instances of the app
|
||||
3. Check available disk space (>200MB required)
|
||||
4. Try installing from a different location
|
||||
|
||||
## Developer Notes
|
||||
|
||||
### Testing the Installer
|
||||
1. Build the .pkg installer
|
||||
2. Test on a clean macOS system or VM
|
||||
3. Verify file associations work immediately
|
||||
4. Check that icons appear in Finder
|
||||
5. Test with both .fbundle and .opat files
|
||||
|
||||
### Customizing Dialogs
|
||||
- Edit `installer-resources/welcome.html` for welcome content
|
||||
- Edit `installer-resources/conclusion.html` for conclusion content
|
||||
- HTML/CSS styling is supported
|
||||
- Keep content concise and user-friendly
|
||||
|
||||
### Modifying Post-Install Script
|
||||
- Edit `installer-scripts/postinstall`
|
||||
- Ensure script remains executable: `chmod +x postinstall`
|
||||
- Test thoroughly on different macOS versions
|
||||
- Add logging for all operations
|
||||
|
||||
## Distribution
|
||||
|
||||
### Recommended Distribution Method
|
||||
1. **Primary**: `.pkg` installer for best user experience
|
||||
2. **Alternative**: `.dmg` for users who prefer disk images
|
||||
3. **Developer**: `.zip` for automated deployment
|
||||
|
||||
### Code Signing (Production)
|
||||
For production distribution:
|
||||
1. Obtain Apple Developer ID certificate
|
||||
2. Configure code signing in package.json
|
||||
3. Notarize the installer with Apple
|
||||
4. Test on systems with Gatekeeper enabled
|
||||
|
||||
## User Support
|
||||
|
||||
### Common User Questions
|
||||
|
||||
**Q: Do I need Docker to use the app?**
|
||||
A: No, Docker is only needed for building plugins. Basic bundle management works without any additional software.
|
||||
|
||||
**Q: Why do I need administrator privileges?**
|
||||
A: The installer needs to install the app to /Applications and register file associations system-wide.
|
||||
|
||||
**Q: Can I install to a different location?**
|
||||
A: The .pkg installer installs to /Applications by default. Use the .dmg version for custom locations.
|
||||
|
||||
**Q: Will this work on older Macs?**
|
||||
A: Yes, the app supports macOS 10.12 and later on both Intel and Apple Silicon Macs.
|
||||
|
||||
This comprehensive installer solution provides a professional, user-friendly installation experience that handles all technical setup automatically while clearly communicating optional dependencies to users.
|
||||
329
electron/generate-icons.js
Normal file
329
electron/generate-icons.js
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Icon Generation Script for 4DSTAR Bundle Manager
|
||||
*
|
||||
* Generates all required macOS icon formats from the SVG source
|
||||
* and creates the proper .icns file for the app bundle.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
class IconGenerator {
|
||||
constructor() {
|
||||
this.projectRoot = path.resolve(__dirname, '..');
|
||||
this.iconOutputDir = path.join(__dirname, 'icons');
|
||||
this.tempDir = path.join(__dirname, 'temp-icons');
|
||||
|
||||
// Icon source paths
|
||||
this.appIconSvg = path.join(this.projectRoot, 'assets', 'toolkit', 'appicon', 'toolkitIcon.svg');
|
||||
this.bundleIconSvg = path.join(this.projectRoot, 'assets', 'bundle', 'fourdst_bundle_icon.svg');
|
||||
this.opatIconSvg = path.join(this.projectRoot, 'assets', 'opat', 'fourdst_opat_icon.svg');
|
||||
|
||||
// macOS app icon sizes (for .icns)
|
||||
this.appIconSizes = [
|
||||
{ size: 16, scale: 1 },
|
||||
{ size: 16, scale: 2 },
|
||||
{ size: 32, scale: 1 },
|
||||
{ size: 32, scale: 2 },
|
||||
{ size: 128, scale: 1 },
|
||||
{ size: 128, scale: 2 },
|
||||
{ size: 256, scale: 1 },
|
||||
{ size: 256, scale: 2 },
|
||||
{ size: 512, scale: 1 },
|
||||
{ size: 512, scale: 2 }
|
||||
];
|
||||
}
|
||||
|
||||
log(message, type = 'info') {
|
||||
const prefix = {
|
||||
'info': '📋',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[type] || 'ℹ️';
|
||||
|
||||
console.log(`${prefix} ${message}`);
|
||||
}
|
||||
|
||||
async runCommand(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(command, args, {
|
||||
stdio: 'pipe',
|
||||
...options
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
checkDependencies() {
|
||||
this.log('Checking dependencies...', 'info');
|
||||
|
||||
// Check if all SVG sources exist
|
||||
const svgSources = [
|
||||
{ name: 'App icon', path: this.appIconSvg },
|
||||
{ name: 'Bundle icon', path: this.bundleIconSvg },
|
||||
{ name: 'OPAT icon', path: this.opatIconSvg }
|
||||
];
|
||||
|
||||
for (const source of svgSources) {
|
||||
if (!fs.existsSync(source.path)) {
|
||||
throw new Error(`${source.name} SVG not found: ${source.path}`);
|
||||
}
|
||||
this.log(`✓ ${source.name} SVG found: ${source.path}`, 'success');
|
||||
}
|
||||
|
||||
// Check for required tools
|
||||
const requiredTools = ['rsvg-convert', 'iconutil'];
|
||||
const missingTools = [];
|
||||
|
||||
for (const tool of requiredTools) {
|
||||
try {
|
||||
this.runCommand('which', [tool]);
|
||||
this.log(`✓ ${tool} available`, 'success');
|
||||
} catch (e) {
|
||||
missingTools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingTools.length > 0) {
|
||||
this.log('Missing required tools. Installing...', 'warning');
|
||||
this.installDependencies(missingTools);
|
||||
}
|
||||
}
|
||||
|
||||
async installDependencies(missingTools) {
|
||||
if (missingTools.includes('rsvg-convert')) {
|
||||
this.log('Installing librsvg (for rsvg-convert)...', 'info');
|
||||
try {
|
||||
await this.runCommand('brew', ['install', 'librsvg']);
|
||||
this.log('✓ librsvg installed', 'success');
|
||||
} catch (e) {
|
||||
throw new Error('Failed to install librsvg. Please install manually: brew install librsvg');
|
||||
}
|
||||
}
|
||||
|
||||
// iconutil is part of Xcode command line tools
|
||||
if (missingTools.includes('iconutil')) {
|
||||
this.log('iconutil not found. Installing Xcode command line tools...', 'info');
|
||||
try {
|
||||
await this.runCommand('xcode-select', ['--install']);
|
||||
this.log('✓ Xcode command line tools installation started', 'success');
|
||||
this.log('Please complete the installation and run this script again', 'warning');
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
throw new Error('Failed to install Xcode command line tools. Please install manually.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async generatePNGIcons() {
|
||||
this.log('Generating PNG icons from SVG...', 'info');
|
||||
|
||||
// Create temp directory
|
||||
if (fs.existsSync(this.tempDir)) {
|
||||
fs.rmSync(this.tempDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(this.tempDir, { recursive: true });
|
||||
|
||||
// Generate app icons
|
||||
this.log(' Generating app icons...', 'info');
|
||||
for (const iconSpec of this.appIconSizes) {
|
||||
const fileName = iconSpec.scale === 1
|
||||
? `app_icon_${iconSpec.size}x${iconSpec.size}.png`
|
||||
: `app_icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
|
||||
|
||||
const outputPath = path.join(this.tempDir, fileName);
|
||||
const actualSize = iconSpec.size * iconSpec.scale;
|
||||
|
||||
this.log(` Generating ${fileName} (${actualSize}x${actualSize})...`, 'info');
|
||||
|
||||
try {
|
||||
await this.runCommand('rsvg-convert', [
|
||||
'--width', actualSize.toString(),
|
||||
'--height', actualSize.toString(),
|
||||
'--format', 'png',
|
||||
'--output', outputPath,
|
||||
this.appIconSvg
|
||||
]);
|
||||
|
||||
this.log(` ✓ ${fileName}`, 'success');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate ${fileName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createAppIconsFile() {
|
||||
this.log('Creating .icns file for macOS app bundle...', 'info');
|
||||
|
||||
const iconsetDir = path.join(this.tempDir, 'app-icon.iconset');
|
||||
if (!fs.existsSync(iconsetDir)) {
|
||||
fs.mkdirSync(iconsetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy PNG files to iconset with proper naming
|
||||
for (const iconSpec of this.appIconSizes) {
|
||||
const sourceFileName = iconSpec.scale === 1
|
||||
? `app_icon_${iconSpec.size}x${iconSpec.size}.png`
|
||||
: `app_icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
|
||||
|
||||
const targetFileName = iconSpec.scale === 1
|
||||
? `icon_${iconSpec.size}x${iconSpec.size}.png`
|
||||
: `icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
|
||||
|
||||
const sourcePath = path.join(this.tempDir, sourceFileName);
|
||||
const targetPath = path.join(iconsetDir, targetFileName);
|
||||
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
// Create .icns file
|
||||
const icnsPath = path.join(this.iconOutputDir, 'app-icon.icns');
|
||||
await this.runCommand('iconutil', [
|
||||
'--convert', 'icns',
|
||||
'--output', icnsPath,
|
||||
iconsetDir
|
||||
]);
|
||||
|
||||
this.log(`✓ Created app-icon.icns: ${icnsPath}`, 'success');
|
||||
|
||||
// Clean up iconset directory
|
||||
fs.rmSync(iconsetDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async createDocumentIcons() {
|
||||
this.log('Generating document type icons...', 'info');
|
||||
|
||||
const documentTypes = [
|
||||
{ name: 'fbundle', filename: 'fbundle-icon.icns', svgSource: this.bundleIconSvg },
|
||||
{ name: 'opat', filename: 'opat-icon.icns', svgSource: this.opatIconSvg }
|
||||
];
|
||||
|
||||
for (const docType of documentTypes) {
|
||||
this.log(` Creating ${docType.name} document icon...`, 'info');
|
||||
|
||||
const iconsetDir = path.join(this.tempDir, `${docType.name}-icon.iconset`);
|
||||
if (!fs.existsSync(iconsetDir)) {
|
||||
fs.mkdirSync(iconsetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate document icons using the specific SVG for each type
|
||||
for (const iconSpec of this.appIconSizes) {
|
||||
const fileName = iconSpec.scale === 1
|
||||
? `icon_${iconSpec.size}x${iconSpec.size}.png`
|
||||
: `icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
|
||||
|
||||
const outputPath = path.join(iconsetDir, fileName);
|
||||
const actualSize = iconSpec.size * iconSpec.scale;
|
||||
|
||||
try {
|
||||
await this.runCommand('rsvg-convert', [
|
||||
'--width', actualSize.toString(),
|
||||
'--height', actualSize.toString(),
|
||||
'--format', 'png',
|
||||
'--output', outputPath,
|
||||
docType.svgSource
|
||||
]);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate ${docType.name} icon ${fileName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create .icns file
|
||||
const icnsPath = path.join(this.iconOutputDir, docType.filename);
|
||||
await this.runCommand('iconutil', [
|
||||
'--convert', 'icns',
|
||||
'--output', icnsPath,
|
||||
iconsetDir
|
||||
]);
|
||||
|
||||
this.log(` ✓ Created ${docType.filename}`, 'success');
|
||||
|
||||
// Clean up iconset directory
|
||||
fs.rmSync(iconsetDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.log('Cleaning up temporary files...', 'info');
|
||||
if (fs.existsSync(this.tempDir)) {
|
||||
fs.rmSync(this.tempDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async generate() {
|
||||
try {
|
||||
this.log('Starting icon generation for 4DSTAR Bundle Manager...', 'info');
|
||||
|
||||
// Create output directory
|
||||
if (!fs.existsSync(this.iconOutputDir)) {
|
||||
fs.mkdirSync(this.iconOutputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
this.checkDependencies();
|
||||
|
||||
// Generate PNG icons
|
||||
await this.generatePNGIcons();
|
||||
|
||||
// Generate .icns files
|
||||
await this.createAppIconsFile();
|
||||
await this.createDocumentIcons();
|
||||
|
||||
// Cleanup
|
||||
this.cleanup();
|
||||
|
||||
this.log('\n🎉 Icon generation completed successfully!', 'success');
|
||||
this.log(`Generated icons in: ${this.iconOutputDir}`, 'info');
|
||||
this.log('Files created:', 'info');
|
||||
this.log(' - app-icon.icns (main app icon)', 'info');
|
||||
this.log(' - fbundle-icon.icns (for .fbundle files)', 'info');
|
||||
this.log(' - opat-icon.icns (for .opat files)', 'info');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Icon generation failed: ${error.message}`, 'error');
|
||||
this.cleanup();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run icon generation if called directly
|
||||
if (require.main === module) {
|
||||
const generator = new IconGenerator();
|
||||
generator.generate().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
}).catch(error => {
|
||||
console.error('Icon generation failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { IconGenerator };
|
||||
BIN
electron/icons/app-icon.icns
Normal file
BIN
electron/icons/app-icon.icns
Normal file
Binary file not shown.
BIN
electron/icons/fbundle-icon.icns
Normal file
BIN
electron/icons/fbundle-icon.icns
Normal file
Binary file not shown.
BIN
electron/icons/opat-icon.icns
Normal file
BIN
electron/icons/opat-icon.icns
Normal file
Binary file not shown.
123
electron/installer-resources/conclusion.html
Normal file
123
electron/installer-resources/conclusion.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #059669;
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
h2 {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.success {
|
||||
background-color: #f0fdf4;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #10b981;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.info {
|
||||
background-color: #f0f9ff;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.next-steps {
|
||||
background-color: #fefce8;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #eab308;
|
||||
margin: 15px 0;
|
||||
}
|
||||
ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.command {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
background-color: #f3f4f6;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.highlight {
|
||||
font-weight: bold;
|
||||
color: #1e40af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Installation Complete!</h1>
|
||||
|
||||
<div class="success">
|
||||
<p><strong>✅ 4DSTAR Bundle Manager has been successfully installed!</strong></p>
|
||||
<p>The application is now available in your Applications folder and file associations have been automatically configured.</p>
|
||||
</div>
|
||||
|
||||
<h2>What's Been Configured</h2>
|
||||
<div class="info">
|
||||
<ul>
|
||||
<li><strong>Application:</strong> Installed to /Applications/4DSTAR Bundle Manager.app</li>
|
||||
<li><strong>File Associations:</strong> .fbundle and .opat files will open with 4DSTAR Bundle Manager</li>
|
||||
<li><strong>Custom Icons:</strong> File icons have been registered and should appear in Finder</li>
|
||||
<li><strong>Launch Services:</strong> Automatically refreshed for immediate functionality</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
<p>You can now:</p>
|
||||
<ul>
|
||||
<li><strong>Launch the app</strong> from Applications or Spotlight</li>
|
||||
<li><strong>Double-click .fbundle files</strong> to open them directly</li>
|
||||
<li><strong>Double-click .opat files</strong> to view them in the OPAT Core section</li>
|
||||
<li><strong>Right-click files</strong> and choose "Open with 4DSTAR Bundle Manager"</li>
|
||||
</ul>
|
||||
|
||||
<div class="next-steps">
|
||||
<h2>Optional: Install Build Dependencies</h2>
|
||||
<p>If you plan to build plugins from source, install these optional tools:</p>
|
||||
<ul>
|
||||
<li><span class="highlight">Docker Desktop:</span> <span class="command">https://docker.com/products/docker-desktop</span></li>
|
||||
<li><span class="highlight">Meson Build System:</span> <span class="command">brew install meson</span></li>
|
||||
<li><span class="highlight">Xcode Command Line Tools:</span> <span class="command">xcode-select --install</span></li>
|
||||
</ul>
|
||||
<p><em>Note: These are not required for basic bundle management and can be installed later.</em></p>
|
||||
</div>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
<div class="info">
|
||||
<p><strong>If file icons don't appear immediately:</strong></p>
|
||||
<ul>
|
||||
<li>Wait a few minutes for macOS to update the icon cache</li>
|
||||
<li>Try logging out and back in to your user account</li>
|
||||
<li>Restart your Mac if icons still don't appear</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>If file associations don't work:</strong></p>
|
||||
<ul>
|
||||
<li>Right-click a .fbundle or .opat file</li>
|
||||
<li>Choose "Get Info" and set 4DSTAR Bundle Manager as the default app</li>
|
||||
<li>Click "Change All..." to apply to all files of that type</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p><strong>Thank you for installing 4DSTAR Bundle Manager!</strong></p>
|
||||
<p>For support and documentation, visit the project repository or contact the development team.</p>
|
||||
</body>
|
||||
</html>
|
||||
125
electron/installer-resources/welcome.html
Normal file
125
electron/installer-resources/welcome.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #1d4ed8;
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
h2 {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #fef3c7;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #f59e0b;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.requirement {
|
||||
background-color: #f0f9ff;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.requirement-title {
|
||||
font-weight: bold;
|
||||
color: #1e40af;
|
||||
}
|
||||
ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.version {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
background-color: #f3f4f6;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to 4DSTAR Bundle Manager</h1>
|
||||
|
||||
<p>This installer will install the 4DSTAR Bundle Manager, a comprehensive tool for managing 4DSTAR plugin bundles and OPAT data files.</p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>What's Included:</strong>
|
||||
<ul>
|
||||
<li>4DSTAR Bundle Manager application</li>
|
||||
<li>File associations for .fbundle and .opat files</li>
|
||||
<li>Custom file icons for associated file types</li>
|
||||
<li>Automatic Launch Services refresh</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>System Requirements</h2>
|
||||
<p>The 4DSTAR Bundle Manager has different requirements depending on how you plan to use it:</p>
|
||||
|
||||
<div class="requirement">
|
||||
<div class="requirement-title">Basic Usage (Bundle Viewing & Management)</div>
|
||||
<ul>
|
||||
<li>macOS 10.12 or later</li>
|
||||
<li>No additional dependencies required</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="requirement">
|
||||
<div class="requirement-title">Plugin Building (Advanced)</div>
|
||||
<p>If you plan to build plugins from source, you'll need:</p>
|
||||
<ul>
|
||||
<li><strong>Docker Desktop:</strong> Required for cross-platform plugin builds</li>
|
||||
<li><strong>Meson Build System:</strong> Required for native plugin compilation</li>
|
||||
<li><strong>C++ Compiler:</strong> Xcode Command Line Tools or equivalent</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Optional Dependencies</h2>
|
||||
<p>These tools are <strong>not required</strong> for basic bundle management, but enable advanced features:</p>
|
||||
|
||||
<div class="requirement">
|
||||
<div class="requirement-title">Docker Desktop</div>
|
||||
<p>Enables cross-platform plugin building for multiple architectures.</p>
|
||||
<ul>
|
||||
<li>Download from: <span class="version">https://docker.com/products/docker-desktop</span></li>
|
||||
<li>Required for: Building plugins for different platforms</li>
|
||||
<li>Alternative: Use pre-built plugins from the community</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="requirement">
|
||||
<div class="requirement-title">Meson Build System</div>
|
||||
<p>Required for native plugin compilation and development.</p>
|
||||
<ul>
|
||||
<li>Install via Homebrew: <span class="version">brew install meson</span></li>
|
||||
<li>Or via pip: <span class="version">pip install meson</span></li>
|
||||
<li>Required for: Native plugin builds and development</li>
|
||||
<li>Alternative: Use Docker builds or pre-built plugins</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>Note:</strong> You can install and use the 4DSTAR Bundle Manager immediately.
|
||||
Docker and Meson can be installed later if you decide to build plugins from source.
|
||||
</div>
|
||||
|
||||
<p>Click <strong>Continue</strong> to proceed with the installation.</p>
|
||||
</body>
|
||||
</html>
|
||||
75
electron/installer-scripts/postinstall
Executable file
75
electron/installer-scripts/postinstall
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Post-install script for 4DSTAR Bundle Manager
|
||||
# This script runs after the app is installed via .pkg installer
|
||||
# It refreshes Launch Services to ensure file associations and icons work immediately
|
||||
|
||||
set -e
|
||||
|
||||
APP_NAME="4DSTAR Bundle Manager"
|
||||
APP_PATH="/Applications/${APP_NAME}.app"
|
||||
LOG_FILE="/tmp/4dstar-postinstall.log"
|
||||
|
||||
# Function to log messages
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_message "Starting post-install script for ${APP_NAME}"
|
||||
|
||||
# Check if app was installed correctly
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
log_message "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_message "App found at: $APP_PATH"
|
||||
|
||||
# Refresh Launch Services database
|
||||
log_message "Refreshing Launch Services database..."
|
||||
|
||||
# Reset Launch Services database
|
||||
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
|
||||
-kill -r -domain local -domain system -domain user 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
# Register the specific app bundle
|
||||
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
|
||||
-f "$APP_PATH" 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
log_message "Launch Services database refreshed"
|
||||
|
||||
# Touch desktop to refresh Finder
|
||||
log_message "Refreshing Finder..."
|
||||
touch "$HOME/Desktop" 2>/dev/null || true
|
||||
|
||||
# Try to restart Finder (may fail if user is not logged in graphically)
|
||||
if pgrep -x "Finder" > /dev/null; then
|
||||
log_message "Restarting Finder to refresh file associations..."
|
||||
killall Finder 2>/dev/null || true
|
||||
sleep 2
|
||||
open -a Finder 2>/dev/null || true
|
||||
else
|
||||
log_message "Finder not running, will refresh on next login"
|
||||
fi
|
||||
|
||||
# Clear icon cache if possible (requires elevated privileges)
|
||||
log_message "Attempting to clear icon cache..."
|
||||
if [ -w "/Library/Caches" ]; then
|
||||
rm -rf "/Library/Caches/com.apple.iconservices.store" 2>/dev/null || true
|
||||
log_message "System icon cache cleared"
|
||||
fi
|
||||
|
||||
# Clear user icon cache
|
||||
if [ -w "$HOME/Library/Caches" ]; then
|
||||
rm -rf "$HOME/Library/Caches/com.apple.iconservices.store" 2>/dev/null || true
|
||||
log_message "User icon cache cleared"
|
||||
fi
|
||||
|
||||
log_message "Post-install script completed successfully"
|
||||
log_message "File associations and icons should now be active"
|
||||
log_message "If icons don't appear immediately, try logging out and back in"
|
||||
|
||||
# Clean up old log files (keep only last 5)
|
||||
find /tmp -name "4dstar-postinstall*.log" -mtime +7 -delete 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
@@ -13,6 +13,7 @@ try {
|
||||
|
||||
let mainWindow;
|
||||
let themeUpdateListener;
|
||||
let pendingFileToOpen = null;
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
@@ -70,7 +71,25 @@ const setupAppEventHandlers = () => {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
app.on('ready', () => {
|
||||
createWindow();
|
||||
|
||||
// Handle any queued file open requests
|
||||
if (pendingFileToOpen) {
|
||||
console.log(`[MAIN_PROCESS] Processing queued file: ${pendingFileToOpen}`);
|
||||
const filePath = pendingFileToOpen;
|
||||
pendingFileToOpen = null;
|
||||
|
||||
// Wait for window to be ready, then open the file
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
handleFileOpen(filePath);
|
||||
});
|
||||
} else {
|
||||
handleFileOpen(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
@@ -88,6 +107,62 @@ const setupAppEventHandlers = () => {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle file associations on macOS
|
||||
app.on('open-file', (event, filePath) => {
|
||||
event.preventDefault();
|
||||
console.log(`[MAIN_PROCESS] Opening file via association: ${filePath}`);
|
||||
|
||||
// If app is not ready yet, queue the file to open later
|
||||
if (!app.isReady()) {
|
||||
console.log(`[MAIN_PROCESS] App not ready, queuing file: ${filePath}`);
|
||||
pendingFileToOpen = filePath;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no window exists, create one first
|
||||
if (!mainWindow) {
|
||||
createWindow();
|
||||
}
|
||||
|
||||
// Wait for window to be ready, then send the file path
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
handleFileOpen(filePath);
|
||||
});
|
||||
} else {
|
||||
handleFileOpen(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle file associations on Windows/Linux via command line args
|
||||
if (process.platform !== 'darwin') {
|
||||
// Check if app was launched with a file argument
|
||||
const fileArg = process.argv.find(arg => arg.endsWith('.fbundle') || arg.endsWith('.opat'));
|
||||
if (fileArg && mainWindow) {
|
||||
handleFileOpen(fileArg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle file opening
|
||||
const handleFileOpen = (filePath) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
console.warn('[MAIN_PROCESS] Cannot open file - main window not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExtension = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (fileExtension === '.fbundle') {
|
||||
console.log(`[MAIN_PROCESS] Opening .fbundle file: ${filePath}`);
|
||||
mainWindow.webContents.send('open-bundle-file', filePath);
|
||||
} else if (fileExtension === '.opat') {
|
||||
console.log(`[MAIN_PROCESS] Opening .opat file: ${filePath}`);
|
||||
mainWindow.webContents.send('open-opat-file', filePath);
|
||||
} else {
|
||||
console.warn(`[MAIN_PROCESS] Unknown file type: ${filePath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const setupThemeHandlers = () => {
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
"main": "main-refactored.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev": "electron .",
|
||||
"validate": "node validate-dependencies.js",
|
||||
"prebuild": "node build-backend.js && node validate-dependencies.js",
|
||||
"generate-icons": "node generate-icons.js",
|
||||
"validate-deps": "node validate-dependencies.js",
|
||||
"check-runtime": "node check-runtime-deps.js",
|
||||
"refresh-icons": "node refresh-macos-icons.js",
|
||||
"prebuild": "node generate-icons.js && node build-backend.js && node validate-dependencies.js",
|
||||
"build": "electron-builder",
|
||||
"prepack": "node build-backend.js && node validate-dependencies.js",
|
||||
"prepack": "node generate-icons.js && node build-backend.js && node validate-dependencies.js",
|
||||
"pack": "electron-builder --dir",
|
||||
"postbuild": "node validate-dependencies.js",
|
||||
"clean": "rm -rf dist build node_modules/.cache"
|
||||
"clean": "rm -rf dist build node_modules/.cache icons temp-icons"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -45,7 +47,7 @@
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"icon": "toolkitIcon.png",
|
||||
"icon": "icons/app-icon.icns",
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*",
|
||||
@@ -56,7 +58,24 @@
|
||||
"!**/test/**/*",
|
||||
"!**/tests/**/*",
|
||||
"!**/*.test.js",
|
||||
"!**/*.spec.js"
|
||||
"!**/*.spec.js",
|
||||
"!temp-icons/**/*"
|
||||
],
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": "fbundle",
|
||||
"name": "4DSTAR Bundle File",
|
||||
"description": "4DSTAR Bundle Archive",
|
||||
"icon": "icons/fbundle-icon.icns",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "opat",
|
||||
"name": "OPAT Data File",
|
||||
"description": "Opacity Project for Asteroseismology and Tidal Evolution Data File",
|
||||
"icon": "icons/opat-icon.icns",
|
||||
"role": "Viewer"
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
@@ -76,6 +95,10 @@
|
||||
"target": "dmg",
|
||||
"arch": ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
"target": "pkg",
|
||||
"arch": ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": ["x64", "arm64"]
|
||||
@@ -84,7 +107,62 @@
|
||||
"hardenedRuntime": true,
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": "entitlements.mac.plist",
|
||||
"entitlementsInherit": "entitlements.mac.plist"
|
||||
"entitlementsInherit": "entitlements.mac.plist",
|
||||
"extendInfo": {
|
||||
"CFBundleDocumentTypes": [
|
||||
{
|
||||
"CFBundleTypeName": "4DSTAR Bundle File",
|
||||
"CFBundleTypeDescription": "4DSTAR Bundle Archive",
|
||||
"CFBundleTypeRole": "Editor",
|
||||
"CFBundleTypeExtensions": ["fbundle"],
|
||||
"CFBundleTypeIconFile": "fbundle-icon.icns",
|
||||
"LSHandlerRank": "Owner",
|
||||
"LSItemContentTypes": ["com.fourdst.fbundle"]
|
||||
},
|
||||
{
|
||||
"CFBundleTypeName": "OPAT Data File",
|
||||
"CFBundleTypeDescription": "Opacity Project for Asteroseismology and Tidal Evolution Data File",
|
||||
"CFBundleTypeRole": "Viewer",
|
||||
"CFBundleTypeExtensions": ["opat"],
|
||||
"CFBundleTypeIconFile": "opat-icon.icns",
|
||||
"LSHandlerRank": "Owner",
|
||||
"LSItemContentTypes": ["com.fourdst.opat"]
|
||||
}
|
||||
],
|
||||
"UTExportedTypeDeclarations": [
|
||||
{
|
||||
"UTTypeIdentifier": "com.fourdst.fbundle",
|
||||
"UTTypeDescription": "4DSTAR Bundle Archive",
|
||||
"UTTypeConformsTo": ["public.data", "public.archive"],
|
||||
"UTTypeTagSpecification": {
|
||||
"public.filename-extension": ["fbundle"],
|
||||
"public.mime-type": ["application/x-fourdst-bundle"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"UTTypeIdentifier": "com.fourdst.opat",
|
||||
"UTTypeDescription": "OPAT Data File",
|
||||
"UTTypeConformsTo": ["public.data"],
|
||||
"UTTypeTagSpecification": {
|
||||
"public.filename-extension": ["opat"],
|
||||
"public.mime-type": ["application/x-opat-data"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": "installer-scripts",
|
||||
"welcome": "installer-resources/welcome.html",
|
||||
"conclusion": "installer-resources/conclusion.html",
|
||||
"allowAnywhere": false,
|
||||
"allowCurrentUserHome": false,
|
||||
"allowRootDirectory": false,
|
||||
"identity": null,
|
||||
"installLocation": "/Applications",
|
||||
"mustClose": [
|
||||
"com.fourdst.bundlemanager"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
|
||||
209
electron/refresh-macos-icons.js
Normal file
209
electron/refresh-macos-icons.js
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* macOS Icon Refresh Script for 4DSTAR Bundle Manager
|
||||
*
|
||||
* This script helps refresh macOS Launch Services database to ensure
|
||||
* file associations and custom icons are properly recognized after
|
||||
* app installation.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class MacOSIconRefresher {
|
||||
constructor() {
|
||||
this.appName = '4DSTAR Bundle Manager';
|
||||
}
|
||||
|
||||
log(message, type = 'info') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = {
|
||||
'info': '📋',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[type] || '📋';
|
||||
|
||||
console.log(`${prefix} [${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
async runCommand(command, args = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}: ${stderr || stdout}`));
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async checkIfMacOS() {
|
||||
if (process.platform !== 'darwin') {
|
||||
throw new Error('This script is only for macOS systems');
|
||||
}
|
||||
this.log('✓ Running on macOS', 'success');
|
||||
}
|
||||
|
||||
async findAppBundle() {
|
||||
const possiblePaths = [
|
||||
`/Applications/${this.appName}.app`,
|
||||
path.join(process.env.HOME, 'Applications', `${this.appName}.app`),
|
||||
path.join(__dirname, 'dist', 'mac', `${this.appName}.app`),
|
||||
path.join(__dirname, 'dist', 'mac-arm64', `${this.appName}.app`)
|
||||
];
|
||||
|
||||
for (const appPath of possiblePaths) {
|
||||
if (fs.existsSync(appPath)) {
|
||||
this.log(`✓ Found app bundle: ${appPath}`, 'success');
|
||||
return appPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Could not find ${this.appName}.app in common locations. Please install the app first.`);
|
||||
}
|
||||
|
||||
async refreshLaunchServices() {
|
||||
this.log('Refreshing Launch Services database...', 'info');
|
||||
|
||||
try {
|
||||
// Reset Launch Services database
|
||||
await this.runCommand('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister', [
|
||||
'-kill',
|
||||
'-r',
|
||||
'-domain', 'local',
|
||||
'-domain', 'system',
|
||||
'-domain', 'user'
|
||||
]);
|
||||
|
||||
this.log('✓ Launch Services database reset', 'success');
|
||||
} catch (error) {
|
||||
this.log(`Warning: Could not reset Launch Services database: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
async registerAppBundle(appPath) {
|
||||
this.log(`Registering app bundle: ${appPath}`, 'info');
|
||||
|
||||
try {
|
||||
// Register the specific app bundle
|
||||
await this.runCommand('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister', [
|
||||
'-f', appPath
|
||||
]);
|
||||
|
||||
this.log('✓ App bundle registered with Launch Services', 'success');
|
||||
} catch (error) {
|
||||
this.log(`Warning: Could not register app bundle: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
async touchDesktop() {
|
||||
this.log('Refreshing desktop and Finder...', 'info');
|
||||
|
||||
try {
|
||||
// Touch the desktop to refresh Finder
|
||||
await this.runCommand('touch', [path.join(process.env.HOME, 'Desktop')]);
|
||||
|
||||
// Kill and restart Finder to refresh file associations
|
||||
await this.runCommand('killall', ['Finder']);
|
||||
|
||||
this.log('✓ Desktop and Finder refreshed', 'success');
|
||||
} catch (error) {
|
||||
this.log(`Warning: Could not refresh Finder: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
async clearIconCache() {
|
||||
this.log('Clearing icon cache...', 'info');
|
||||
|
||||
try {
|
||||
// Clear icon cache
|
||||
const iconCachePaths = [
|
||||
path.join(process.env.HOME, 'Library/Caches/com.apple.iconservices.store'),
|
||||
'/Library/Caches/com.apple.iconservices.store',
|
||||
'/System/Library/Caches/com.apple.iconservices.store'
|
||||
];
|
||||
|
||||
for (const cachePath of iconCachePaths) {
|
||||
if (fs.existsSync(cachePath)) {
|
||||
try {
|
||||
await this.runCommand('sudo', ['rm', '-rf', cachePath]);
|
||||
this.log(`✓ Cleared icon cache: ${cachePath}`, 'success');
|
||||
} catch (error) {
|
||||
this.log(`Could not clear ${cachePath}: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Warning: Could not clear all icon caches: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.log(`Starting macOS icon refresh for ${this.appName}...`, 'info');
|
||||
|
||||
// Check if we're on macOS
|
||||
await this.checkIfMacOS();
|
||||
|
||||
// Find the app bundle
|
||||
const appPath = await this.findAppBundle();
|
||||
|
||||
// Refresh Launch Services
|
||||
await this.refreshLaunchServices();
|
||||
|
||||
// Register the app bundle
|
||||
await this.registerAppBundle(appPath);
|
||||
|
||||
// Clear icon cache (requires sudo)
|
||||
this.log('Note: Icon cache clearing may require sudo password', 'info');
|
||||
await this.clearIconCache();
|
||||
|
||||
// Refresh desktop and Finder
|
||||
await this.touchDesktop();
|
||||
|
||||
this.log('\n🎉 macOS icon refresh completed!', 'success');
|
||||
this.log('File associations and icons should now be updated.', 'info');
|
||||
this.log('If icons still don\'t appear, try logging out and back in.', 'info');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Icon refresh failed: ${error.message}`, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run icon refresh if called directly
|
||||
if (require.main === module) {
|
||||
const refresher = new MacOSIconRefresher();
|
||||
refresher.refresh().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = MacOSIconRefresher;
|
||||
@@ -12,6 +12,12 @@ async function handleOpenBundle() {
|
||||
const bundlePath = await ipcRenderer.invoke('select-file');
|
||||
if (!bundlePath) return;
|
||||
|
||||
await openBundleFromPath(bundlePath);
|
||||
}
|
||||
|
||||
async function openBundleFromPath(bundlePath) {
|
||||
if (!bundlePath) return;
|
||||
|
||||
// Small delay to ensure file dialog closes properly
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -249,6 +255,7 @@ function initializeDependencies(deps) {
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
handleOpenBundle,
|
||||
openBundleFromPath,
|
||||
handleSignBundle,
|
||||
handleValidateBundle,
|
||||
handleClearBundle,
|
||||
|
||||
@@ -85,11 +85,19 @@ function initializeDOMElements() {
|
||||
|
||||
// --- VIEW AND UI LOGIC ---
|
||||
function showView(viewId) {
|
||||
// Get the OPAT view element
|
||||
const opatView = document.getElementById('opat-view');
|
||||
|
||||
// Hide main content views
|
||||
[welcomeScreen, bundleView, createBundleForm].forEach(view => {
|
||||
view.classList.toggle('hidden', view.id !== viewId);
|
||||
});
|
||||
|
||||
// Handle OPAT view separately since it's not in the main views array
|
||||
if (opatView) {
|
||||
opatView.classList.toggle('hidden', viewId !== 'opat-view');
|
||||
}
|
||||
|
||||
// Also hide all category home screens when showing main content
|
||||
const categoryHomeScreens = [
|
||||
'libplugin-home', 'opat-home', 'libconstants-home', 'serif-home'
|
||||
@@ -108,6 +116,14 @@ function showView(viewId) {
|
||||
if (libpluginView) {
|
||||
libpluginView.classList.remove('hidden');
|
||||
}
|
||||
} else if (viewId === 'opat-view') {
|
||||
// Ensure OPAT view is visible and properly initialized
|
||||
if (opatView) {
|
||||
opatView.classList.remove('hidden');
|
||||
console.log('[DOM_MANAGER] OPAT view shown successfully');
|
||||
} else {
|
||||
console.error('[DOM_MANAGER] OPAT view element not found!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Import dependencies (these will be injected when integrated)
|
||||
let stateManager, domManager, bundleOperations, fillWorkflow, uiComponents;
|
||||
let stateManager, domManager, bundleOperations, fillWorkflow, uiComponents, opatHandler;
|
||||
|
||||
// --- EVENT LISTENERS SETUP ---
|
||||
function setupEventListeners() {
|
||||
@@ -15,6 +15,49 @@ function setupEventListeners() {
|
||||
document.body.classList.toggle('dark-mode', shouldUseDarkColors);
|
||||
});
|
||||
|
||||
// File association handlers
|
||||
ipcRenderer.on('open-bundle-file', async (event, filePath) => {
|
||||
console.log(`[RENDERER] Opening .fbundle file via association: ${filePath}`);
|
||||
try {
|
||||
// Switch to libplugin category if not already there
|
||||
const libpluginCategory = document.querySelector('.category-item[data-category="libplugin"]');
|
||||
if (libpluginCategory && !libpluginCategory.classList.contains('active')) {
|
||||
libpluginCategory.click();
|
||||
}
|
||||
|
||||
// Open the bundle
|
||||
await bundleOperations.openBundleFromPath(filePath);
|
||||
} catch (error) {
|
||||
console.error('[RENDERER] Error opening bundle file:', error);
|
||||
domManager.showModal('File Open Error', `Failed to open bundle file: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-opat-file', async (event, filePath) => {
|
||||
console.log(`[RENDERER] Opening .opat file via association: ${filePath}`);
|
||||
try {
|
||||
// Switch to OPAT Core category
|
||||
const opatCategory = document.querySelector('.category-item[data-category="opat"]');
|
||||
if (opatCategory) {
|
||||
opatCategory.click();
|
||||
}
|
||||
|
||||
// Wait a moment for category switching to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Open the OPAT file using the OPAT handler
|
||||
if (opatHandler && opatHandler.openOpatFromPath) {
|
||||
await opatHandler.openOpatFromPath(filePath);
|
||||
} else {
|
||||
console.warn('[RENDERER] OPAT file opening not available');
|
||||
domManager.showModal('Error', 'OPAT file opening functionality is not available.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RENDERER] Error opening OPAT file:', error);
|
||||
domManager.showModal('File Open Error', `Failed to open OPAT file: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar navigation
|
||||
elements.openBundleBtn.addEventListener('click', bundleOperations.handleOpenBundle);
|
||||
elements.createBundleBtn.addEventListener('click', () => {
|
||||
@@ -375,6 +418,7 @@ function initializeDependencies(deps) {
|
||||
bundleOperations = deps.bundleOperations;
|
||||
fillWorkflow = deps.fillWorkflow;
|
||||
uiComponents = deps.uiComponents;
|
||||
opatHandler = deps.opatHandler;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -7,9 +7,18 @@ let stateManager, domManager, opatPlotting;
|
||||
// OPAT File Inspector variables
|
||||
let opatFileInput, opatBrowseBtn, opatView, opatCloseBtn;
|
||||
let opatHeaderInfo, opatAllTagsList, opatIndexSelector, opatTablesDisplay, opatTableDataContent;
|
||||
let opatElementsInitialized = false;
|
||||
|
||||
// Initialize OPAT UI elements
|
||||
function initializeOPATElements() {
|
||||
console.log('[OPAT_HANDLER] initializeOPATElements called, already initialized:', opatElementsInitialized);
|
||||
|
||||
// Prevent duplicate initialization
|
||||
if (opatElementsInitialized) {
|
||||
console.log('[OPAT_HANDLER] OPAT elements already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
opatFileInput = document.getElementById('opat-file-input');
|
||||
opatBrowseBtn = document.getElementById('opat-browse-btn');
|
||||
opatView = document.getElementById('opat-view');
|
||||
@@ -20,11 +29,41 @@ function initializeOPATElements() {
|
||||
opatTablesDisplay = document.getElementById('opat-tables-display');
|
||||
opatTableDataContent = document.getElementById('opat-table-data-content');
|
||||
|
||||
console.log('[OPAT_HANDLER] Found elements:', {
|
||||
opatFileInput: !!opatFileInput,
|
||||
opatBrowseBtn: !!opatBrowseBtn,
|
||||
opatView: !!opatView,
|
||||
opatCloseBtn: !!opatCloseBtn
|
||||
});
|
||||
|
||||
// Event listeners
|
||||
opatBrowseBtn.addEventListener('click', () => opatFileInput.click());
|
||||
opatFileInput.addEventListener('change', handleOPATFileSelection);
|
||||
opatIndexSelector.addEventListener('change', handleIndexVectorChange);
|
||||
opatCloseBtn.addEventListener('click', closeOPATFile);
|
||||
if (opatBrowseBtn) {
|
||||
console.log('[OPAT_HANDLER] Adding click listener to browse button');
|
||||
opatBrowseBtn.addEventListener('click', () => {
|
||||
console.log('[OPAT_HANDLER] Browse button clicked, triggering file input');
|
||||
if (opatFileInput) {
|
||||
opatFileInput.click();
|
||||
} else {
|
||||
console.error('[OPAT_HANDLER] File input element not found!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (opatFileInput) {
|
||||
console.log('[OPAT_HANDLER] Adding change listener to file input');
|
||||
opatFileInput.addEventListener('change', handleOPATFileSelection);
|
||||
}
|
||||
|
||||
if (opatIndexSelector) {
|
||||
opatIndexSelector.addEventListener('change', handleIndexVectorChange);
|
||||
}
|
||||
|
||||
if (opatCloseBtn) {
|
||||
opatCloseBtn.addEventListener('click', closeOPATFile);
|
||||
}
|
||||
|
||||
opatElementsInitialized = true;
|
||||
console.log('[OPAT_HANDLER] OPAT elements initialization complete');
|
||||
|
||||
// Initialize OPAT tab navigation
|
||||
initializeOPATTabs();
|
||||
@@ -96,48 +135,171 @@ function resetOPATViewerState() {
|
||||
|
||||
// Handle OPAT file selection
|
||||
async function handleOPATFileSelection(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
console.log('[OPAT_HANDLER] ===== FILE SELECTION EVENT TRIGGERED =====');
|
||||
console.log('[OPAT_HANDLER] Event target:', event.target);
|
||||
console.log('[OPAT_HANDLER] Files array:', event.target.files);
|
||||
console.log('[OPAT_HANDLER] Number of files:', event.target.files ? event.target.files.length : 0);
|
||||
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
console.log('[OPAT_HANDLER] No file selected - event fired but no file found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[OPAT_HANDLER] File selected:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: new Date(file.lastModified)
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Loading OPAT file:', file.name);
|
||||
domManager.showSpinner();
|
||||
console.log('[OPAT_HANDLER] Starting file processing...');
|
||||
|
||||
// Reset the viewer state
|
||||
console.log('[OPAT_HANDLER] Resetting viewer state...');
|
||||
resetOPATViewerState();
|
||||
|
||||
// Show the OPAT view first to ensure UI is visible
|
||||
console.log('[OPAT_HANDLER] Showing OPAT view...');
|
||||
domManager.showView('opat-view');
|
||||
|
||||
// Read and parse the file
|
||||
console.log('[OPAT_HANDLER] Reading file as ArrayBuffer...');
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const currentOPATFile = parseOPAT(arrayBuffer);
|
||||
stateManager.setOPATFile(currentOPATFile);
|
||||
console.log('[OPAT_HANDLER] File read successfully, arrayBuffer size:', arrayBuffer.byteLength);
|
||||
|
||||
displayOPATFileInfo();
|
||||
displayAllTableTags();
|
||||
populateIndexSelector();
|
||||
// Check if parseOPAT is available
|
||||
console.log('[OPAT_HANDLER] Checking parseOPAT availability...');
|
||||
console.log('[OPAT_HANDLER] typeof parseOPAT:', typeof parseOPAT);
|
||||
console.log('[OPAT_HANDLER] window.parseOPAT:', typeof window.parseOPAT);
|
||||
|
||||
// Populate plotting selectors if module is available
|
||||
if (opatPlotting) {
|
||||
opatPlotting.populatePlotIndexSelector();
|
||||
if (typeof parseOPAT === 'undefined' && typeof window.parseOPAT === 'undefined') {
|
||||
throw new Error('parseOPAT function is not available. Make sure opatParser.js is loaded.');
|
||||
}
|
||||
|
||||
// Show OPAT view
|
||||
hideAllViews();
|
||||
opatView.classList.remove('hidden');
|
||||
// Use global parseOPAT if local one is undefined
|
||||
const parseFunction = typeof parseOPAT !== 'undefined' ? parseOPAT : window.parseOPAT;
|
||||
console.log('[OPAT_HANDLER] Using parse function:', typeof parseFunction);
|
||||
|
||||
domManager.hideSpinner();
|
||||
console.log('OPAT file loaded successfully');
|
||||
console.log('[OPAT_HANDLER] Calling parseOPAT...');
|
||||
const currentOPATFile = parseFunction(arrayBuffer);
|
||||
console.log('[OPAT_HANDLER] Parse result:', currentOPATFile ? 'SUCCESS' : 'FAILED');
|
||||
console.log('[OPAT_HANDLER] Parsed file object:', currentOPATFile);
|
||||
|
||||
if (currentOPATFile) {
|
||||
console.log('[OPAT_HANDLER] Setting file in state manager...');
|
||||
stateManager.setOPATFile(currentOPATFile);
|
||||
|
||||
// Display file information
|
||||
console.log('[OPAT_HANDLER] Displaying file information...');
|
||||
displayOPATFileInfo();
|
||||
displayAllTableTags();
|
||||
populateIndexSelector();
|
||||
|
||||
console.log('[OPAT_HANDLER] ===== OPAT FILE LOADED SUCCESSFULLY =====');
|
||||
} else {
|
||||
console.error('[OPAT_HANDLER] parseOPAT returned null/undefined');
|
||||
domManager.showModal('Error', 'Failed to parse OPAT file. Please check the file format.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading OPAT file:', error);
|
||||
domManager.hideSpinner();
|
||||
alert('Error loading OPAT file: ' + error.message);
|
||||
console.error('[OPAT_HANDLER] ===== ERROR IN FILE PROCESSING =====');
|
||||
console.error('[OPAT_HANDLER] Error details:', error);
|
||||
console.error('[OPAT_HANDLER] Error stack:', error.stack);
|
||||
domManager.showModal('Error', `Failed to load OPAT file: ${error.message}`);
|
||||
} finally {
|
||||
console.log('[OPAT_HANDLER] Cleaning up file input...');
|
||||
// Clear the file input to prevent issues with reopening the same file
|
||||
if (event.target) {
|
||||
event.target.value = '';
|
||||
console.log('[OPAT_HANDLER] File input cleared');
|
||||
}
|
||||
console.log('[OPAT_HANDLER] ===== FILE SELECTION HANDLER COMPLETE =====');
|
||||
}
|
||||
}
|
||||
|
||||
// Open OPAT file from file path (for file associations)
|
||||
async function openOpatFromPath(filePath) {
|
||||
if (!filePath) {
|
||||
console.log('[OPAT_HANDLER] openOpatFromPath: No file path provided');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[OPAT_HANDLER] Opening OPAT file from path:', filePath);
|
||||
|
||||
// Ensure OPAT UI elements are initialized
|
||||
console.log('[OPAT_HANDLER] Initializing OPAT UI elements...');
|
||||
initializeOPATElements();
|
||||
initializeOPATTabs();
|
||||
|
||||
// Reset the viewer state
|
||||
resetOPATViewerState();
|
||||
|
||||
// Show the OPAT view first to ensure UI is visible
|
||||
console.log('[OPAT_HANDLER] Showing OPAT view...');
|
||||
domManager.showView('opat-view');
|
||||
|
||||
// Read the file using Node.js fs
|
||||
const fs = require('fs');
|
||||
console.log('[OPAT_HANDLER] Reading file from disk...');
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
||||
console.log('[OPAT_HANDLER] File read successfully, arrayBuffer size:', arrayBuffer.byteLength);
|
||||
|
||||
// Parse the OPAT file
|
||||
console.log('[OPAT_HANDLER] Parsing OPAT file...');
|
||||
if (typeof parseOPAT === 'undefined') {
|
||||
throw new Error('parseOPAT function is not available. Make sure opatParser.js is loaded.');
|
||||
}
|
||||
const currentOPATFile = parseOPAT(arrayBuffer);
|
||||
console.log('[OPAT_HANDLER] Parse result:', currentOPATFile ? 'SUCCESS' : 'FAILED');
|
||||
|
||||
if (currentOPATFile) {
|
||||
console.log('[OPAT_HANDLER] Setting file in state manager...');
|
||||
stateManager.setOPATFile(currentOPATFile);
|
||||
|
||||
// Display file information
|
||||
console.log('[OPAT_HANDLER] Displaying file information...');
|
||||
displayOPATFileInfo();
|
||||
displayAllTableTags();
|
||||
populateIndexSelector();
|
||||
|
||||
console.log('[OPAT_HANDLER] OPAT file opened successfully via file association');
|
||||
} else {
|
||||
console.error('[OPAT_HANDLER] parseOPAT returned null/undefined for file association');
|
||||
throw new Error('Failed to parse OPAT file. Please check the file format.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OPAT_HANDLER] Error opening OPAT file via file association:', error);
|
||||
domManager.showModal('Error', `Failed to open OPAT file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Display OPAT file information
|
||||
function displayOPATFileInfo() {
|
||||
console.log('[OPAT_HANDLER] displayOPATFileInfo called');
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
if (!currentOPATFile) return;
|
||||
console.log('[OPAT_HANDLER] Current OPAT file from state:', currentOPATFile);
|
||||
|
||||
if (!currentOPATFile) {
|
||||
console.error('[OPAT_HANDLER] No OPAT file in state manager!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[OPAT_HANDLER] opatHeaderInfo element:', opatHeaderInfo);
|
||||
console.log('[OPAT_HANDLER] opatHeaderInfo exists:', !!opatHeaderInfo);
|
||||
|
||||
if (!opatHeaderInfo) {
|
||||
console.error('[OPAT_HANDLER] opatHeaderInfo element not found! Re-initializing...');
|
||||
opatHeaderInfo = document.getElementById('opat-header-info');
|
||||
console.log('[OPAT_HANDLER] After re-init, opatHeaderInfo:', !!opatHeaderInfo);
|
||||
}
|
||||
|
||||
const header = currentOPATFile.header;
|
||||
opatHeaderInfo.innerHTML = `
|
||||
console.log('[OPAT_HANDLER] Header object:', header);
|
||||
|
||||
const headerHTML = `
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">Header Information</h4>
|
||||
<div class="info-grid">
|
||||
@@ -155,14 +317,42 @@ function displayOPATFileInfo() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
console.log('[OPAT_HANDLER] Generated header HTML length:', headerHTML.length);
|
||||
|
||||
if (opatHeaderInfo) {
|
||||
opatHeaderInfo.innerHTML = headerHTML;
|
||||
console.log('[OPAT_HANDLER] Header info updated successfully');
|
||||
console.log('[OPAT_HANDLER] opatHeaderInfo.innerHTML length:', opatHeaderInfo.innerHTML.length);
|
||||
} else {
|
||||
console.error('[OPAT_HANDLER] Cannot update header info - element still not found');
|
||||
}
|
||||
|
||||
// Display all unique table tags
|
||||
console.log('[OPAT_HANDLER] Calling displayAllTableTags...');
|
||||
displayAllTableTags();
|
||||
}
|
||||
|
||||
// Display all table tags
|
||||
function displayAllTableTags() {
|
||||
console.log('[OPAT_HANDLER] displayAllTableTags called');
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
if (!currentOPATFile) return;
|
||||
console.log('[OPAT_HANDLER] Current OPAT file in displayAllTableTags:', currentOPATFile);
|
||||
|
||||
if (!currentOPATFile) {
|
||||
console.error('[OPAT_HANDLER] No OPAT file in displayAllTableTags!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[OPAT_HANDLER] opatAllTagsList element:', opatAllTagsList);
|
||||
console.log('[OPAT_HANDLER] opatAllTagsList exists:', !!opatAllTagsList);
|
||||
|
||||
if (!opatAllTagsList) {
|
||||
console.error('[OPAT_HANDLER] opatAllTagsList element not found! Re-initializing...');
|
||||
opatAllTagsList = document.getElementById('opat-all-tags-list');
|
||||
console.log('[OPAT_HANDLER] After re-init, opatAllTagsList:', !!opatAllTagsList);
|
||||
}
|
||||
|
||||
console.log('[OPAT_HANDLER] Number of cards:', currentOPATFile.cards.size);
|
||||
|
||||
const allTags = new Set();
|
||||
for (const card of currentOPATFile.cards.values()) {
|
||||
@@ -171,12 +361,19 @@ function displayAllTableTags() {
|
||||
}
|
||||
}
|
||||
|
||||
opatAllTagsList.innerHTML = '';
|
||||
Array.from(allTags).sort().forEach(tag => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = tag;
|
||||
opatAllTagsList.appendChild(li);
|
||||
});
|
||||
console.log('[OPAT_HANDLER] Found', allTags.size, 'unique tags:', Array.from(allTags));
|
||||
|
||||
if (opatAllTagsList) {
|
||||
opatAllTagsList.innerHTML = '';
|
||||
Array.from(allTags).sort().forEach(tag => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = tag;
|
||||
opatAllTagsList.appendChild(li);
|
||||
});
|
||||
console.log('[OPAT_HANDLER] Tags list updated successfully');
|
||||
} else {
|
||||
console.error('[OPAT_HANDLER] Cannot update tags list - element still not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Populate index selector
|
||||
@@ -398,6 +595,7 @@ module.exports = {
|
||||
initializeOPATTabs,
|
||||
resetOPATViewerState,
|
||||
handleOPATFileSelection,
|
||||
openOpatFromPath,
|
||||
displayOPATFileInfo,
|
||||
displayAllTableTags,
|
||||
populateIndexSelector,
|
||||
|
||||
249
electron/test-file-associations.js
Normal file
249
electron/test-file-associations.js
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* File Association Test Script for 4DSTAR Bundle Manager
|
||||
*
|
||||
* This script tests that the app correctly handles file association events
|
||||
* for .fbundle and .opat files when they are opened via the OS.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class FileAssociationTester {
|
||||
constructor() {
|
||||
this.appPath = path.join(__dirname, 'dist', 'mac-arm64', '4DSTAR Bundle Manager.app');
|
||||
this.testFilesDir = path.join(__dirname, 'test-files');
|
||||
}
|
||||
|
||||
log(message, type = 'info') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = {
|
||||
'info': '📋',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[type] || '📋';
|
||||
|
||||
console.log(`${prefix} [${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
async createTestFiles() {
|
||||
this.log('Creating test files for file association testing...', 'info');
|
||||
|
||||
// Create test files directory
|
||||
if (!fs.existsSync(this.testFilesDir)) {
|
||||
fs.mkdirSync(this.testFilesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a minimal test .fbundle file (ZIP format)
|
||||
const fbundlePath = path.join(this.testFilesDir, 'test-bundle.fbundle');
|
||||
const fbundleContent = Buffer.from('PK\x03\x04'); // ZIP file header
|
||||
fs.writeFileSync(fbundlePath, fbundleContent);
|
||||
this.log(`✓ Created test .fbundle file: ${fbundlePath}`, 'success');
|
||||
|
||||
// Create a minimal test .opat file
|
||||
const opatPath = path.join(this.testFilesDir, 'test-data.opat');
|
||||
const opatContent = Buffer.alloc(100); // Minimal binary file
|
||||
fs.writeFileSync(opatPath, opatContent);
|
||||
this.log(`✓ Created test .opat file: ${opatPath}`, 'success');
|
||||
|
||||
return { fbundlePath, opatPath };
|
||||
}
|
||||
|
||||
async testAppExists() {
|
||||
this.log('Checking if app bundle exists...', 'info');
|
||||
|
||||
if (!fs.existsSync(this.appPath)) {
|
||||
throw new Error(`App bundle not found at: ${this.appPath}`);
|
||||
}
|
||||
|
||||
this.log(`✓ App bundle found: ${this.appPath}`, 'success');
|
||||
return true;
|
||||
}
|
||||
|
||||
async testFileAssociation(filePath, fileType) {
|
||||
this.log(`Testing ${fileType} file association: ${path.basename(filePath)}`, 'info');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Use 'open' command to simulate double-clicking the file
|
||||
const openProcess = spawn('open', [filePath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
openProcess.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
openProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
openProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.log(`✓ ${fileType} file opened successfully`, 'success');
|
||||
resolve({ success: true, stdout, stderr });
|
||||
} else {
|
||||
this.log(`✗ ${fileType} file failed to open (exit code: ${code})`, 'error');
|
||||
if (stderr) this.log(`Error: ${stderr}`, 'error');
|
||||
resolve({ success: false, stdout, stderr, code });
|
||||
}
|
||||
});
|
||||
|
||||
openProcess.on('error', (error) => {
|
||||
this.log(`✗ Error opening ${fileType} file: ${error.message}`, 'error');
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
openProcess.kill();
|
||||
this.log(`⚠️ ${fileType} file association test timed out`, 'warning');
|
||||
resolve({ success: false, timeout: true });
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async testAppLaunch() {
|
||||
this.log('Testing direct app launch...', 'info');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const appProcess = spawn('open', [this.appPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
appProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
appProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.log('✓ App launched successfully', 'success');
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
this.log(`✗ App failed to launch (exit code: ${code})`, 'error');
|
||||
if (stderr) this.log(`Error: ${stderr}`, 'error');
|
||||
resolve({ success: false, code, stderr });
|
||||
}
|
||||
});
|
||||
|
||||
appProcess.on('error', (error) => {
|
||||
this.log(`✗ Error launching app: ${error.message}`, 'error');
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Timeout after 15 seconds
|
||||
setTimeout(() => {
|
||||
appProcess.kill();
|
||||
this.log('⚠️ App launch test timed out', 'warning');
|
||||
resolve({ success: false, timeout: true });
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
async runTests() {
|
||||
try {
|
||||
this.log('Starting file association tests for 4DSTAR Bundle Manager...', 'info');
|
||||
|
||||
// Test 1: Check if app exists
|
||||
await this.testAppExists();
|
||||
|
||||
// Test 2: Create test files
|
||||
const { fbundlePath, opatPath } = await this.createTestFiles();
|
||||
|
||||
// Test 3: Test direct app launch
|
||||
const launchResult = await this.testAppLaunch();
|
||||
|
||||
// Wait a moment for app to fully start
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Test 4: Test .fbundle file association
|
||||
const fbundleResult = await this.testFileAssociation(fbundlePath, '.fbundle');
|
||||
|
||||
// Wait between tests
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Test 5: Test .opat file association
|
||||
const opatResult = await this.testFileAssociation(opatPath, '.opat');
|
||||
|
||||
// Summary
|
||||
this.log('\n=== TEST RESULTS SUMMARY ===', 'info');
|
||||
this.log(`App Launch: ${launchResult.success ? '✅ PASS' : '❌ FAIL'}`, launchResult.success ? 'success' : 'error');
|
||||
this.log(`Bundle File Association: ${fbundleResult.success ? '✅ PASS' : '❌ FAIL'}`, fbundleResult.success ? 'success' : 'error');
|
||||
this.log(`OPAT File Association: ${opatResult.success ? '✅ PASS' : '❌ FAIL'}`, opatResult.success ? 'success' : 'error');
|
||||
|
||||
const allPassed = launchResult.success && fbundleResult.success && opatResult.success;
|
||||
|
||||
if (allPassed) {
|
||||
this.log('\n🎉 All file association tests PASSED!', 'success');
|
||||
this.log('The app correctly handles file associations for both .fbundle and .opat files.', 'success');
|
||||
} else {
|
||||
this.log('\n⚠️ Some tests FAILED. Check the logs above for details.', 'warning');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
this.log('\nCleaning up test files...', 'info');
|
||||
if (fs.existsSync(this.testFilesDir)) {
|
||||
fs.rmSync(this.testFilesDir, { recursive: true, force: true });
|
||||
this.log('✓ Test files cleaned up', 'success');
|
||||
}
|
||||
|
||||
return allPassed;
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Test suite failed: ${error.message}`, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkFileAssociations() {
|
||||
this.log('Checking macOS file associations...', 'info');
|
||||
|
||||
try {
|
||||
// Check what app is associated with .fbundle files
|
||||
const fbundleCheck = spawn('duti', ['-x', 'fbundle'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
fbundleCheck.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.log('✓ .fbundle file association registered', 'success');
|
||||
} else {
|
||||
this.log('⚠️ .fbundle file association may not be registered', 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
// Check what app is associated with .opat files
|
||||
const opatCheck = spawn('duti', ['-x', 'opat'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
opatCheck.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.log('✓ .opat file association registered', 'success');
|
||||
} else {
|
||||
this.log('⚠️ .opat file association may not be registered', 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.log('⚠️ Could not check file associations (duti not available)', 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
const tester = new FileAssociationTester();
|
||||
|
||||
tester.runTests().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
}).catch(error => {
|
||||
console.error('Test suite crashed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = FileAssociationTester;
|
||||
Reference in New Issue
Block a user