Merge branch 'adoptable-discoverability.outdated-package-display-fixes'

This commit is contained in:
David Bureš 2025-12-06 20:19:10 +01:00
commit 2514db5e47
No known key found for this signature in database
225 changed files with 5779 additions and 1406 deletions

View File

@ -12,6 +12,7 @@ import SwiftUI
import CorkShared
import Defaults
import DefaultsMacros
import CorkModels
@Observable
class AppDelegate: NSObject, NSApplicationDelegate

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "custom.shippingbox.2.badge.arrow.down.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
<!--glyph: "", point size: 100.0, font version: "21.0d6e2", template writer version: "138.0.0"-->
<style>.defaults {-sfsymbols-variable-value-mode:color;-sfsymbols-draw-reverses-motion-groups:true}
.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:3be001059164cad3 6d5e10b98f62d4b7 shippingbox}
.monochrome-1 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.monochrome-2 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.monochrome-3 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:3be001059164cad3 6d5e10b98f62d4b7 shippingbox}
.multicolor-1:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.multicolor-2:tintColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.multicolor-3:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:3be001059164cad3 6d5e10b98f62d4b7 shippingbox}
.hierarchical-1:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.hierarchical-2:primary {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.hierarchical-3:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:3be001059164cad3 _badge arrow.down}
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
</style>
<g id="Notes">
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
<g transform="matrix(0.2 0 0 0.2 263 1933)">
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm3.61328-17.7734v-28.4668c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v28.4668c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094Zm-17.8223-10.5957h28.418c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-28.418c-2.24609 0-3.75977 1.51367-3.75977 3.71094 0 2.14844 1.51367 3.61328 3.75977 3.61328Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm4.05273-23.0957v-36.9141c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v36.9141c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039Zm-22.5586-14.4043h36.9629c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-36.9629c-2.49023 0-4.15039 1.70898-4.15039 4.15039 0 2.39258 1.66016 4.00391 4.15039 4.00391Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm4.44336-30.3223v-48.4863c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v48.4863c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984Zm-28.7109-19.7754h48.4863c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-48.4863c-2.73438 0-4.58984 1.85547-4.58984 4.58984 0 2.58789 1.85547 4.39453 4.58984 4.39453Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
<g transform="matrix(0.2 0 0 0.2 776 1933)">
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l20.5566-57.5195h0.244141l20.6055 57.5195c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm10.2051-20.9473h30.6641c2.00195 0 3.66211-1.66016 3.66211-3.66211 0-2.05078-1.66016-3.66211-3.66211-3.66211h-30.6641c-2.00195 0-3.66211 1.61133-3.66211 3.66211 0 2.00195 1.66016 3.66211 3.66211 3.66211Z"/>
</g>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
<path d="m14.209 13.1348 7.86133 7.86133c4.29688 4.39453 9.32617 4.10156 13.8672-1.02539l60.6934-68.2129-4.88281-4.88281-60.2539 67.6758c-1.80664 1.95312-3.4668 2.44141-5.81055 0.0976562l-5.17578-5.12695c-2.29492-2.29492-1.80664-3.95508 0.195312-5.81055l67.4805-62.1582-4.88281-4.83398-68.0664 62.5977c-4.98047 4.58984-5.32227 9.47266-1.02539 13.8184Zm44.873-97.4609c-2.05078 2.00195-2.24609 4.88281-1.07422 6.78711 1.12305 1.80664 3.4668 3.02734 6.5918 2.24609 5.85938-1.66016 12.5977-2.39258 18.8965 0.927734l-2.68555 7.12891c-1.61133 4.00391-0.732422 6.88477 1.70898 9.42383l10.2539 10.3027c2.34375 2.39258 4.54102 2.44141 7.08008 1.95312l4.44336-0.732422 2.58789 2.53906-0.195312 2.24609c-0.0976562 2.29492 0.537109 4.29688 2.7832 6.49414l3.36914 3.32031c2.29492 2.29492 5.51758 2.49023 7.8125 0.195312l12.9883-13.0371c2.29492-2.34375 2.14844-5.37109-0.195312-7.66602l-3.41797-3.41797c-2.19727-2.19727-4.05273-3.02734-6.34766-2.88086l-2.34375 0.244141-2.44141-2.44141 1.02539-4.6875c0.634766-2.73438-0.244141-4.98047-2.88086-7.61719l-11.2793-11.1816c-12.9395-12.8418-35.5957-11.0352-46.6797-0.146484Zm7.08008 2.05078c8.78906-6.39648 25.9766-5.66406 33.6914 1.95312l12.3047 12.207c1.02539 1.02539 1.2207 1.80664 0.927734 3.32031l-1.46484 6.64062 6.73828 6.68945 4.39453-0.244141c1.12305-0.0488281 1.51367 0.0488281 2.34375 0.878906l2.53906 2.49023-10.8398 10.8398-2.49023-2.49023c-0.830078-0.878906-0.976562-1.2207-0.927734-2.39258l0.292969-4.3457-6.68945-6.73828-6.83594 1.17188c-1.41602 0.292969-2.05078 0.195312-3.17383-0.878906l-8.93555-8.88672c-1.07422-1.02539-1.17188-1.70898-0.488281-3.36914l4.58984-11.4746c-6.10352-6.34766-17.041-7.51953-25.5859-4.58984-0.683594 0.244141-0.927734-0.390625-0.390625-0.78125Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
</g>
<g id="Guides">
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2983.79" x2="2983.79" y1="600.785" y2="720.121"/>
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2883.01" x2="2883.01" y1="600.785" y2="720.121"/>
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1498.5" x2="1498.5" y1="600.785" y2="720.121"/>
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1401.19" x2="1401.19" y1="600.785" y2="720.121"/>
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="606.961" x2="606.961" y1="600.785" y2="720.121"/>
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="512.462" x2="512.462" y1="600.785" y2="720.121"/>
</g>
<g id="Symbols">
<g id="Black-S" transform="matrix(1 0 0 1 2883.01 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M15.9668-9.375L47.168 6.78711C49.0234 7.76367 51.7578 7.76367 53.6133 6.78711L84.8145-9.375C89.3066-11.7188 91.0156-14.5508 91.0156-19.5312L91.0156-53.6621C91.0156-58.2031 89.2578-61.2793 84.9121-63.5254L58.8379-77.1484C53.6621-79.8828 47.1191-79.8828 41.9434-77.1484L15.8691-63.5254C11.5234-61.2793 9.76562-58.2031 9.76562-53.6621L9.76562-19.5312C9.76562-14.5508 11.4746-11.7188 15.9668-9.375ZM23.584-19.3359C22.0703-20.166 21.4844-21.0938 21.4844-22.6074L21.4844-45.6055L44.2383-33.7891L44.2383-8.30078ZM77.1973-19.3359L56.543-8.30078L56.543-33.7891L79.2969-45.6055L79.2969-22.6074C79.2969-21.0938 78.7109-20.166 77.1973-19.3359ZM50.3906-44.4824L28.2227-56.0547L33.252-58.6426L55.5664-47.168ZM68.1152-53.7598L46.0449-65.332L47.4121-66.0156C49.3164-66.9922 51.416-67.041 53.3691-66.0156L72.5586-56.0547Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M90.7019 20.249C105.643 20.249 117.997 7.84668 117.997-7.0947C117.997-22.0361 105.643-34.3408 90.7019-34.3408C75.7605-34.3408 63.407-22.0361 63.407-7.0947C63.407 7.84668 75.7605 20.249 90.7019 20.249Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M90.7019 13.9014C102.177 13.9014 111.649 4.33105 111.649-7.0947C111.649-18.5205 102.177-27.9932 90.7019-27.9932C79.2273-27.9932 69.7546-18.5205 69.7546-7.0947C69.7546 4.33105 79.2273 13.9014 90.7019 13.9014Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M90.7019 6.08886C89.53 6.08886 88.4558 5.69824 87.3816 4.62402L78.2507-4.16504C77.4695-4.8975 77.03-5.874 77.03-6.9482C77.03-9.2432 78.8855-11.1475 81.2292-11.1475C82.3523-11.1475 83.5242-10.708 84.1101-9.8779L86.2585-6.9482L86.3074-6.8994L86.1609-9.5361L86.1609-15.7861C86.1609-18.4229 87.9675-20.1807 90.7019-20.1807C93.4363-20.1807 95.2429-18.4229 95.2429-15.7861L95.2429-9.5361L95.0964-6.8994L95.1453-6.9482L97.2937-9.8779C97.8796-10.708 99.0515-11.1475 100.175-11.1475C102.518-11.1475 104.374-9.2432 104.374-6.9482C104.374-5.874 103.934-4.8975 103.153-4.16504L94.0222 4.62402C92.948 5.69824 91.8738 6.08886 90.7019 6.08886Z"/>
</g>
<g id="Regular-S" transform="matrix(1 0 0 1 1401.19 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M15.5273-10.5469L45.3125 6.49414C47.6074 7.8125 49.707 7.8125 51.9531 6.49414L81.7383-10.5469C85.4492-12.6465 87.5488-14.7461 87.5488-20.8496L87.5488-50.3418C87.5488-54.7852 85.9375-57.5684 82.3242-59.668L56.2012-74.6582C51.0742-77.5879 46.2402-77.5879 41.1133-74.6582L14.9902-59.668C11.3281-57.5684 9.76562-54.7852 9.76562-50.3418L9.76562-20.8496C9.76562-14.7461 11.8652-12.6465 15.5273-10.5469ZM19.2871-16.3574C17.1875-17.5781 16.3574-18.8965 16.3574-21.0449L16.3574-48.9746L45.2148-32.2754L45.2148-1.41602ZM78.0273-16.3574L52.0996-1.41602L52.0996-32.2754L80.957-48.9746L80.957-21.0449C80.957-18.8965 80.0781-17.5781 78.0273-16.3574ZM48.6328-38.3789L20.2148-54.6387L31.0547-60.9375L59.4238-44.5801ZM66.4551-48.584L37.8906-64.8438L43.8477-68.2617C47.168-70.166 50.1465-70.2148 53.418-68.2617L77.0996-54.6387Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M87.5791 18.3448C101.446 18.3448 112.969 6.82131 112.969-7.04585C112.969-20.9618 101.544-32.4365 87.5791-32.4365C73.6631-32.4365 62.1885-20.9618 62.1885-7.04585C62.1885 6.91896 73.6142 18.3448 87.5791 18.3448Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M87.5791 12.6319C98.2236 12.6319 107.306 3.69631 107.306-7.04585C107.306-17.8368 98.3701-26.7236 87.5791-26.7236C76.7881-26.7236 67.9013-17.8368 67.9013-7.04585C67.9013 3.79396 76.7881 12.6319 87.5791 12.6319Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M87.6279 5.25881C86.9443 5.25881 86.3584 4.96584 85.626 4.28225L75.7138-4.84865C75.1279-5.33685 74.8838-5.97165 74.8349-6.70405C74.7373-8.16895 75.9092-9.19435 77.374-9.19435C78.0576-9.24315 78.79-8.85255 79.2783-8.36425L83.2822-4.31151L85.0888-2.45604L84.8447-7.14355L84.8447-16.665C84.8447-18.1298 86.1142-19.3505 87.6279-19.3505C89.1416-19.3505 90.4599-18.1298 90.4599-16.665L90.4599-7.14355L90.2158-2.45604L91.9736-4.31151L95.9775-8.36425C96.5146-8.85255 97.1982-9.14545 97.8818-9.19435C99.2978-9.29195 100.421-8.16895 100.421-6.70405C100.421-5.97165 100.177-5.33685 99.5908-4.84865L89.6299 4.28225C88.8974 4.91701 88.3603 5.25881 87.6279 5.25881Z"/>
</g>
<g id="Ultralight-S" transform="matrix(1 0 0 1 512.462 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M13.3023-13.6347L44.7676 3.49709C46.4268 4.36136 47.9815 4.40677 49.728 3.49709L81.1934-13.6347C83.8145-15.0986 84.7334-16.6987 84.7334-19.5781L84.7334-53.4297C84.7334-55.103 83.7578-56.2515 82.3242-57.0796L51.1153-74.3403C48.6675-75.6807 45.8315-75.6807 43.3838-74.3403L12.1748-57.0796C10.7378-56.2515 9.76562-55.103 9.76562-53.4297L9.76562-19.5781C9.76562-16.6987 10.6392-15.0986 13.3023-13.6347ZM15.2002-15.0859C12.5103-16.5791 12.1343-17.5796 12.1343-19.7734L12.1343-52.789L46.0776-34.3642L46.0776 1.53562ZM79.2988-15.0859L48.376 1.53562L48.376-34.3642L82.3647-52.789L82.3647-19.7734C82.3647-17.5796 81.9853-16.5791 79.2988-15.0859ZM47.2251-36.2901L12.7676-54.9565L29.1475-63.98L63.7832-45.2612ZM66.0918-46.4951L31.4424-65.2524L44.9829-72.7119C46.4414-73.5718 48.0576-73.5752 49.5127-72.7119L81.686-54.9565Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M85.0492 14.3033C96.7367 14.3033 106.398 4.68705 106.398-7.04587C106.398-18.7822 96.7888-28.3951 85.0492-28.3951C73.3582-28.3951 63.7-18.7368 63.7-7.04587C63.7 4.69389 73.3548 14.3033 85.0492 14.3033Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M85.0492 11.2242C95.0578 11.2242 103.368 3.01517 103.368-7.04587C103.368-17.1104 95.1135-25.3159 85.0492-25.3159C75.0755-25.3159 66.7791-17.065 66.7791-7.04587C66.7791 3.02201 75.0755 11.2242 85.0492 11.2242Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M85.098 5.07717C84.7777 5.07717 84.4642 4.96584 84.0949 4.60012L74.3646-4.98487C73.824-5.51857 73.5799-5.83547 73.5764-6.38617C73.5696-7.16987 74.1966-7.74117 74.9803-7.74117C75.3914-7.74457 75.7606-7.53567 76.0218-7.27437L77.9823-5.21967L83.8304 0.631837L83.7225-5.05467L83.7225-18.209C83.7225-18.9019 84.3563-19.5322 85.098-19.5322C85.8851-19.5322 86.4769-18.9019 86.4769-18.209L86.4769-5.05467L86.4144 0.631837L92.2136-5.21967L94.2195-7.27437C94.3934-7.53567 94.8045-7.73777 95.2157-7.74117C96.0413-7.74807 96.6194-7.16987 96.6194-6.38617C96.6194-5.88087 96.3753-5.51857 95.8348-4.98487L86.1008 4.60012C85.8226 4.87161 85.5125 5.07717 85.098 5.07717Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -11,6 +11,8 @@ import ButtonKit
import CorkShared
import Defaults
import SwiftUI
import CorkModels
import CorkTerminalFunctions
struct ContentView: View, Sendable
{
@ -426,6 +428,9 @@ private extension View
case .tapAddition:
AddTapView()
case .massAppAdoption(let appsToAdopt):
MassAppAdoptionView(appsToAdopt: appsToAdopt)
case .fullUpdate:
UpdatePackagesView()

View File

@ -15,6 +15,8 @@ import Defaults
import SwiftData
import SwiftUI
import UserNotifications
import CorkModels
import CorkTerminalFunctions
@main
struct CorkApp: App
@ -90,7 +92,8 @@ struct CorkApp: App
.environment(outdatedPackagesTracker)
.environment(topPackagesTracker)
.modelContainer(for: [
SavedTaggedPackage.self
SavedTaggedPackage.self,
ExcludedAdoptableApp.self
])
.task
{
@ -131,146 +134,19 @@ struct CorkApp: App
}
.onAppear
{
print("Licensing state: \(appDelegate.appState.licensingState)")
#if SELF_COMPILED
AppConstants.shared.logger.debug("Will set licensing state to Self Compiled")
appDelegate.appState.licensingState = .selfCompiled
#else
if !hasValidatedEmail
{
if appDelegate.appState.licensingState != .selfCompiled
{
if let demoActivatedAt
{
let timeDemoWillRunOutAt: Date = demoActivatedAt + AppConstants.shared.demoLengthInSeconds
AppConstants.shared.logger.debug("There is \(demoActivatedAt.timeIntervalSinceNow.formatted()) to go on the demo")
AppConstants.shared.logger.debug("Demo will time out at \(timeDemoWillRunOutAt.formatted(date: .complete, time: .complete))")
if ((demoActivatedAt.timeIntervalSinceNow) + AppConstants.shared.demoLengthInSeconds) > 0
{ // Check if there is still time on the demo
/// do stuff if there is
}
else
{
hasFinishedLicensingWorkflow = false
}
}
}
}
#endif
handleLicensing()
}
.onAppear
{
// Start the background update scheduler when the app starts
backgroundUpdateTimer.schedule
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
Task
{
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
AppConstants.shared.logger.debug("Update result:\nStandard output: \(updateResult.standardOutput, privacy: .public)\nStandard error: \(updateResult.standardError, privacy: .public)")
do
{
let temporaryOutdatedPackageTracker: OutdatedPackagesTracker = await .init()
try await temporaryOutdatedPackageTracker.getOutdatedPackages(brewPackagesTracker: brewPackagesTracker)
var newOutdatedPackages: Set<OutdatedPackage> = await temporaryOutdatedPackageTracker.outdatedPackages
AppConstants.shared.logger.debug("Outdated packages checker output: \(newOutdatedPackages, privacy: .public)")
defer
{
AppConstants.shared.logger.log("Will purge temporary update trackers")
updateResult = .init(standardOutput: "", standardError: "")
newOutdatedPackages = .init()
}
if await newOutdatedPackages.count > outdatedPackagesTracker.outdatedPackages.count
{
AppConstants.shared.logger.log("New updates found")
/// Set this to `true` so the normal notification doesn't get sent
await setWhetherToSendStandardUpdatesAvailableNotification(to: false)
let differentPackages: Set<OutdatedPackage> = await newOutdatedPackages.subtracting(outdatedPackagesTracker.displayableOutdatedPackages)
AppConstants.shared.logger.debug("Changed packages: \(differentPackages, privacy: .auto)")
sendNotification(title: String(localized: "notification.new-outdated-packages-found.title"), subtitle: differentPackages.map(\.package.name).formatted(.list(type: .and)))
await outdatedPackagesTracker.setOutdatedPackages(to: newOutdatedPackages)
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
sendStandardUpdatesAvailableNotification = true
}
}
else
{
AppConstants.shared.logger.log("No new updates found")
}
}
catch
{
AppConstants.shared.logger.error("Something got fucked up about checking for outdated packages")
}
}
completion(NSBackgroundActivityScheduler.Result.finished)
}
handleBackgroundUpdating()
}
.onChange(of: demoActivatedAt) // React to when the user activates the demo
{ _, newValue in
if let newValue
{ // If the demo has not been activated, `demoActivatedAt` is nil. So, when it's not nil anymore, it means the user activated it
AppConstants.shared.logger.debug("The user activated the demo at \(newValue.formatted(date: .complete, time: .complete), privacy: .public)")
hasFinishedLicensingWorkflow = true
}
handleDemoTiming(newValue: newValue)
}
.onChange(of: outdatedPackagesTracker.displayableOutdatedPackages.count)
{ _, outdatedPackageCount in
AppConstants.shared.logger.debug("Number of displayable outdated packages changed (\(outdatedPackageCount))")
// TODO: Remove this once I figure out why the updating spinner sometimes doesn't disappear
withAnimation
{
outdatedPackagesTracker.isCheckingForPackageUpdates = false
}
if outdatedPackageCount == 0
{
NSApp.dockTile.badgeLabel = ""
}
else
{
if areNotificationsEnabled
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
{
NSApp.dockTile.badgeLabel = String(outdatedPackageCount)
}
// TODO: Changing the package display type sends a notificaiton, which is not visible since the app is in the foreground. Once macOS 15 comes out, move `sendStandardUpdatesAvailableNotification` into the AppState and suppress it
if outdatedPackageNotificationType == .notification || outdatedPackageNotificationType == .both
{
AppConstants.shared.logger.log("Will try to send notification")
/// This needs to be checked because when the background update system finds an update, we don't want to send this normal notification.
/// Instead, we want to send a more succinct notification that includes only the new package
if sendStandardUpdatesAvailableNotification
{
sendNotification(title: String(localized: "notification.outdated-packages-found.title"), subtitle: String(localized: "notification.outdated-packages-found.body-\(outdatedPackageCount)"))
}
}
}
}
handleOutdatedPackageChangeAppBadge(outdatedPackageCount: outdatedPackageCount)
}
.onChange(of: outdatedPackageNotificationType) // Set the correct app badge number when the user changes their notification settings
{ _, newValue in
@ -415,7 +291,7 @@ struct CorkApp: App
WindowGroup(id: .previewWindowID, for: MinimalHomebrewPackage.self)
{ $packageToPreview in
let convertedMinimalPackage: BrewPackage? = .init(from: packageToPreview)
let convertedMinimalPackage: BrewPackage? = BrewPackage(using: packageToPreview)
PackagePreview(packageToPreview: convertedMinimalPackage)
.navigationTitle(packageToPreview?.name ?? "")
@ -657,13 +533,8 @@ struct CorkApp: App
.environment(appDelegate.appState)
.environment(outdatedPackagesTracker)
Button
{
appDelegate.appState.showSheet(ofType: .fullUpdate)
} label: {
Text("navigation.menu.packages.update")
}
.keyboardShortcut("r", modifiers: [.control, .command])
UpgradePackagesButton(appState: appDelegate.appState)
.keyboardShortcut("r", modifiers: [.control, .command])
}
@ViewBuilder
@ -721,7 +592,7 @@ struct CorkApp: App
{
Button
{
openWindow(id: .errorInspectorWindowID, value: PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
openWindow(id: .errorInspectorWindowID, value: BrewPackage.PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
} label: {
Text("debug.action.show-error-inspector")
}
@ -729,9 +600,10 @@ struct CorkApp: App
Text("debug.action.ui")
}
}
// MARK: - Functions
// MARK: - App badge
func setAppBadge(outdatedPackageNotificationType: OutdatedPackageNotificationType)
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
@ -746,9 +618,160 @@ struct CorkApp: App
NSApp.dockTile.badgeLabel = ""
}
}
private func setWhetherToSendStandardUpdatesAvailableNotification(to newValue: Bool)
{
self.sendStandardUpdatesAvailableNotification = newValue
}
func handleOutdatedPackageChangeAppBadge(outdatedPackageCount: Int)
{
AppConstants.shared.logger.debug("Number of displayable outdated packages changed (\(outdatedPackageCount))")
// TODO: Remove this once I figure out why the updating spinner sometimes doesn't disappear
withAnimation
{
outdatedPackagesTracker.isCheckingForPackageUpdates = false
}
if outdatedPackageCount == 0
{
NSApp.dockTile.badgeLabel = ""
}
else
{
if areNotificationsEnabled
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
{
NSApp.dockTile.badgeLabel = String(outdatedPackageCount)
}
// TODO: Changing the package display type sends a notificaiton, which is not visible since the app is in the foreground. Once macOS 15 comes out, move `sendStandardUpdatesAvailableNotification` into the AppState and suppress it
if outdatedPackageNotificationType == .notification || outdatedPackageNotificationType == .both
{
AppConstants.shared.logger.log("Will try to send notification")
/// This needs to be checked because when the background update system finds an update, we don't want to send this normal notification.
/// Instead, we want to send a more succinct notification that includes only the new package
if sendStandardUpdatesAvailableNotification
{
sendNotification(title: String(localized: "notification.outdated-packages-found.title"), subtitle: String(localized: "notification.outdated-packages-found.body-\(outdatedPackageCount)"))
}
}
}
}
}
// MARK: - Background updating
func handleBackgroundUpdating()
{
// Start the background update scheduler when the app starts
backgroundUpdateTimer.schedule
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
Task
{
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
AppConstants.shared.logger.debug("Update result:\nStandard output: \(updateResult.standardOutput, privacy: .public)\nStandard error: \(updateResult.standardError, privacy: .public)")
do
{
let temporaryOutdatedPackageTracker: OutdatedPackagesTracker = await .init()
try await temporaryOutdatedPackageTracker.getOutdatedPackages(brewPackagesTracker: brewPackagesTracker)
var newOutdatedPackages: Set<OutdatedPackage> = await temporaryOutdatedPackageTracker.outdatedPackages
AppConstants.shared.logger.debug("Outdated packages checker output: \(newOutdatedPackages, privacy: .public)")
defer
{
AppConstants.shared.logger.log("Will purge temporary update trackers")
updateResult = .init(standardOutput: "", standardError: "")
newOutdatedPackages = .init()
}
if await newOutdatedPackages.count > outdatedPackagesTracker.outdatedPackages.count
{
AppConstants.shared.logger.log("New updates found")
/// Set this to `true` so the normal notification doesn't get sent
await setWhetherToSendStandardUpdatesAvailableNotification(to: false)
let differentPackages: Set<OutdatedPackage> = await newOutdatedPackages.subtracting(outdatedPackagesTracker.displayableOutdatedPackages)
AppConstants.shared.logger.debug("Changed packages: \(differentPackages, privacy: .auto)")
sendNotification(title: String(localized: "notification.new-outdated-packages-found.title"), subtitle: differentPackages.map(\.package.name).formatted(.list(type: .and)))
await outdatedPackagesTracker.setOutdatedPackages(to: newOutdatedPackages)
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
sendStandardUpdatesAvailableNotification = true
}
}
else
{
AppConstants.shared.logger.log("No new updates found")
}
}
catch
{
AppConstants.shared.logger.error("Something got fucked up about checking for outdated packages")
}
}
completion(NSBackgroundActivityScheduler.Result.finished)
}
}
// MARK: - Licensing
func handleLicensing()
{
print("Licensing state: \(appDelegate.appState.licensingState)")
#if SELF_COMPILED
AppConstants.shared.logger.debug("Will set licensing state to Self Compiled")
appDelegate.appState.licensingState = .selfCompiled
#else
if !hasValidatedEmail
{
if appDelegate.appState.licensingState != .selfCompiled
{
if let demoActivatedAt
{
let timeDemoWillRunOutAt: Date = demoActivatedAt + AppConstants.shared.demoLengthInSeconds
AppConstants.shared.logger.debug("There is \(demoActivatedAt.timeIntervalSinceNow.formatted()) to go on the demo")
AppConstants.shared.logger.debug("Demo will time out at \(timeDemoWillRunOutAt.formatted(date: .complete, time: .complete))")
if ((demoActivatedAt.timeIntervalSinceNow) + AppConstants.shared.demoLengthInSeconds) > 0
{ // Check if there is still time on the demo
/// do stuff if there is
}
else
{
hasFinishedLicensingWorkflow = false
}
}
}
}
#endif
}
func handleDemoTiming(newValue: Date?)
{
if let newValue
{ // If the demo has not been activated, `demoActivatedAt` is nil. So, when it's not nil anymore, it means the user activated it
AppConstants.shared.logger.debug("The user activated the demo at \(newValue.formatted(date: .complete, time: .complete), privacy: .public)")
hasFinishedLicensingWorkflow = true
}
}
}

View File

@ -1,13 +0,0 @@
//
// Brewfile Import Stage.swift
// Cork
//
// Created by David Bureš on 11.11.2023.
//
import Foundation
enum BrewfileImportStage
{
case importing, finished
}

View File

@ -1,18 +0,0 @@
//
// Licensing State.swift
// Cork
//
// Created by David Bureš on 18.03.2024.
//
import Foundation
enum LicensingState
{
case notBoughtOrHasNotActivatedDemo
case demo
case bought
case selfCompiled
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
enum NavigationTargetMainWindow: Hashable
{

View File

@ -1,48 +0,0 @@
//
// Outdated Package Type.swift
// Cork
//
// Created by David Bureš on 17.05.2024.
//
import Charts
import Foundation
import SwiftUI
enum CachedDownloadType: String, CustomStringConvertible, Plottable
{
case formula
case cask
case other
case unknown
var description: String
{
switch self
{
case .formula:
return String(localized: "package-details.type.formula")
case .cask:
return String(localized: "package-details.type.cask")
case .other:
return String(localized: "start-page.cached-downloads.graph.other-smaller-packages")
default:
return String(localized: "cached-downloads.type.unknown")
}
}
var color: Color
{
switch self
{
case .formula:
return .purple
case .cask:
return .orange
case .other:
return .mint
default:
return .gray
}
}
}

View File

@ -1,79 +0,0 @@
//
// Package Types.swift
// Cork
//
// Created by David Bureš on 05.02.2023.
//
import AppIntents
import Charts
import CorkShared
import Foundation
import SwiftUI
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
{
case formula
case cask
/// User-readable description of the package type
var description: String
{
switch self
{
case .formula:
return String(localized: "package-details.type.formula")
case .cask:
return String(localized: "package-details.type.cask")
}
}
/// Localization keys for description of the package type
var localizableDescription: LocalizedStringKey
{
switch self
{
case .formula:
return "package-details.type.formula"
case .cask:
return "package-details.type.cask"
}
}
/// Parent folder for this package type
var parentFolder: URL
{
switch self
{
case .formula:
return AppConstants.shared.brewCellarPath
case .cask:
return AppConstants.shared.brewCaskPath
}
}
/// Accessibility representation
var accessibilityLabel: LocalizedStringKey
{
switch self
{
case .formula:
return "accessibility.label.package-type.formula"
case .cask:
return "accessibility.label.package-type.cask"
}
}
static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "package-details.type")
var displayRepresentation: DisplayRepresentation
{
switch self
{
case .formula:
DisplayRepresentation(title: "package-details.type.formula")
case .cask:
DisplayRepresentation(title: "package-details.type.cask")
}
}
}

View File

@ -1,24 +0,0 @@
//
// JSON Parsing Error.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
import Foundation
enum JSONParsingError: LocalizedError
{
case couldNotConvertStringToData(failureReason: String?), couldNotDecode(failureReason: String)
var errorDescription: String?
{
switch self
{
case .couldNotConvertStringToData(let failureReason):
return String(localized: "error.json-parsing.could-not-convert-string-to-data.\(failureReason ?? "")")
case .couldNotDecode(let failureReason):
return String(localized: "error.json-parsing.could-not-decode.\(failureReason)")
}
}
}

View File

@ -1,70 +0,0 @@
//
// Package Loading Error.swift
// Cork
//
// Created by David Bureš on 10.11.2024.
//
import Foundation
/// Error representing failures while loading
enum PackageLoadingError: LocalizedError, Hashable, Identifiable
{
/// Tried to treat the folder `Cellar` or `Caskroom` itself as a package - means Homebrew itself is broken
case triedToThreatFolderContainingPackagesAsPackage(packageType: PackageType)
/// The `Cellar` and `Caskroom` folder itself couldn't be loaded
case couldNotReadContentsOfParentFolder(failureReason: String, folderURL: URL)
/// Failed while trying to read contents of package folder
case failedWhileReadingContentsOfPackageFolder(folderURL: URL, reportedError: String)
case failedWhileTryingToDetermineIntentionalInstallation(folderURL: URL, associatedIntentionalDiscoveryError: IntentionalInstallationDiscoveryError)
/// The package root folder exists, but the package itself doesn't have any versions
case packageDoesNotHaveAnyVersionsInstalled(packageURL: URL)
/// A folder that should have contained the package is not actually a folder
case packageIsNotAFolder(String, packageURL: URL)
/// The number of loaded packages does not match the number of package parent folders
case numberOLoadedPackagesDosNotMatchNumberOfPackageFolders
var errorDescription: String?
{
switch self
{
case .couldNotReadContentsOfParentFolder(let failureReason, _):
return String(localized: "error.package-loading.could-not-read-contents-of-parent-folder.\(failureReason)")
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
switch packageType
{
case .formula:
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"
case .cask:
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"
}
case .failedWhileReadingContentsOfPackageFolder(let folderURL, let reportedError):
return String(localized: "error.package-loading.could-not-load-\(folderURL.packageNameFromURL())-at-\(folderURL.absoluteString)-because-\(reportedError)", comment: "Couldn't load package (package name) at (package URL) because (failure reason)")
case .failedWhileTryingToDetermineIntentionalInstallation(_, let associatedIntentionalDiscoveryError):
return associatedIntentionalDiscoveryError.localizedDescription
case .packageDoesNotHaveAnyVersionsInstalled(let packageURL):
return String(localized: "error.package-loading.\(packageURL.packageNameFromURL())-does-not-have-any-versions-installed")
case .packageIsNotAFolder(let string, _):
return String(localized: "error.package-loading.\(string)-not-a-folder", comment: "Package folder in this context means a folder that encloses package versions. Every package has its own folder, and this error occurs when the provided URL does not point to a folder that encloses package versions")
case .numberOLoadedPackagesDosNotMatchNumberOfPackageFolders:
return String(localized: "error.package-loading.number-of-loaded-poackages-does-not-match-number-of-package-folders", comment: "This error occurs when there's a mismatch between the number of loaded packages, and the number of package folders in the package folders")
}
}
var id: UUID
{
return UUID()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
//
// Adopt Package.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
extension MassAppAdoptionView.MassAppAdoptionTacker
{
@MainActor
func adoptApp(
_ appToAdopt: BrewPackagesTracker.AdoptableApp
) async -> AdoptionProcessResult
{
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--cask", "--adopt", appToAdopt.caskName])
adoptionProcess = process
var consolidatedOutput: (standardOutput: [String], standardError: [String]) = (standardOutput: .init(), standardError: .init())
for await output in stream
{
switch output {
case .standardOutput(let string):
self.outputLines.append(.init(line: string))
consolidatedOutput.standardOutput.append(string)
case .standardError(let string):
self.outputLines.append(.init(line: string))
consolidatedOutput.standardError.append(string)
}
}
AppConstants.shared.logger.debug("""
Finished mass adoption process for cask \(appToAdopt.caskName) with this result:
Output: \(consolidatedOutput.standardOutput.joined())
Error: \(consolidatedOutput.standardError.joined())
""")
if consolidatedOutput.standardError.isEmpty
{
AppConstants.shared.logger.info("Adoption process for cask \(appToAdopt.caskName) was successful")
return .success(appToAdopt)
}
else
{
AppConstants.shared.logger.error("Adoption process for cask \(appToAdopt.caskName) failed")
return .failure(
.failedWithError(
failedAdoptionCandidate: appToAdopt,
error: consolidatedOutput.standardError.joined()
)
)
}
}
}

View File

@ -8,16 +8,20 @@
import AppIntents
import Foundation
import CorkShared
import CorkModels
import CorkIntents
struct GetInstalledCasksIntent: AppIntent
public struct GetInstalledCasksIntent: AppIntent
{
static let title: LocalizedStringResource = "intent.get-installed-casks.title"
static let description: LocalizedStringResource = "intent.get-installed-casks.description"
public init() {}
public static let title: LocalizedStringResource = "intent.get-installed-casks.title"
public static let description: LocalizedStringResource = "intent.get-installed-casks.description"
static let isDiscoverable: Bool = true
static let openAppWhenRun: Bool = false
public static let isDiscoverable: Bool = true
public static let openAppWhenRun: Bool = false
func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
public func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
{
let allowAccessToFile: Bool = AppConstants.shared.brewCaskPath.startAccessingSecurityScopedResource()

View File

@ -8,6 +8,7 @@
import AppIntents
import Foundation
import CorkShared
import CorkModels
enum FolderAccessingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import AppIntents
import Foundation
import CorkModels
struct GetInstalledPackagesIntent: AppIntent
{

View File

@ -8,6 +8,7 @@
import AppIntents
import Foundation
import CorkShared
import CorkTerminalFunctions
enum RefreshIntentResult: String, AppEnum
{

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
enum BrewfileDumpingError: LocalizedError
{

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
enum BrewfileReadingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkModels
enum TopPackageLoadingError: LocalizedError
{
@ -99,7 +100,15 @@ extension TopPackagesTracker
if normalizedDownloadNumber > downloadsCutoff
{
return .init(name: rawTopFormula.formula, type: .formula, installedOn: nil, versions: .init(), sizeInBytes: nil, downloadCount: normalizedDownloadNumber)
return .init(
name: rawTopFormula.formula,
type: .formula,
installedOn: nil,
versions: .init(),
url: nil,
sizeInBytes: nil,
downloadCount: normalizedDownloadNumber
)
}
else
{
@ -155,7 +164,15 @@ extension TopPackagesTracker
if normalizedDownloadNumber > downloadsCutoff
{
return .init(name: rawTopCask.cask, type: .cask, installedOn: nil, versions: .init(), sizeInBytes: nil, downloadCount: normalizedDownloadNumber)
return .init(
name: rawTopCask.cask,
type: .cask,
installedOn: nil,
versions: .init(),
url: nil,
sizeInBytes: nil,
downloadCount: normalizedDownloadNumber
)
}
else
{

View File

@ -1,177 +0,0 @@
//
// Get Contents of Folder.swift
// Cork
//
// Created by David Bureš on 03.07.2022.
//
import Foundation
import SwiftUI
import CorkShared
/*
func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
{
do
{
guard let items = targetFolder.validPackageURLs
else
{
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "alert.fatal.could-not-filter-invalid-packages"))
}
let loadedPackages: Set<BrewPackage> = try await withThrowingTaskGroup(of: BrewPackage.self, returning: Set<BrewPackage>.self)
{ taskGroup in
for item in items
{
let fullURLToPackageFolderCurrentlyBeingProcessed: URL = targetFolder.appendingPathComponent(item, conformingTo: .folder)
taskGroup.addTask(priority: .high)
{
guard let versionURLs: [URL] = fullURLToPackageFolderCurrentlyBeingProcessed.packageVersionURLs
else
{
if targetFolder.appendingPathComponent(item, conformingTo: .fileURL).isDirectory
{
AppConstants.shared.logger.error("Failed while getting package version for package \(fullURLToPackageFolderCurrentlyBeingProcessed.lastPathComponent). Package does not have any version installed.")
throw PackageLoadingError.packageDoesNotHaveAnyVersionsInstalled(item)
}
else
{
AppConstants.shared.logger.error("Failed while getting package version for package \(fullURLToPackageFolderCurrentlyBeingProcessed.lastPathComponent). Package is not a folder")
throw PackageLoadingError.packageIsNotAFolder(item, targetFolder.appendingPathComponent(item, conformingTo: .fileURL))
}
}
do
{
if versionURLs.isEmpty
{
throw PackageLoadingError.packageDoesNotHaveAnyVersionsInstalled(item)
}
let wasPackageInstalledIntentionally: Bool = try await targetFolder.checkIfPackageWasInstalledIntentionally(versionURLs)
let foundPackage: BrewPackage = .init(
name: item,
type: targetFolder.packageType,
installedOn: fullURLToPackageFolderCurrentlyBeingProcessed.creationDate,
versions: versionURLs.versions,
installedIntentionally: wasPackageInstalledIntentionally,
sizeInBytes: fullURLToPackageFolderCurrentlyBeingProcessed.directorySize
)
return foundPackage
}
catch
{
throw error
}
}
}
var loadedPackages: Set<BrewPackage> = .init()
for try await package in taskGroup
{
loadedPackages.insert(package)
}
return loadedPackages
}
return loadedPackages
}
catch
{
AppConstants.shared.logger.error("Failed while accessing folder: \(error)")
throw error
}
}
*/
// MARK: - Sub-functions
private extension URL
{
/// ``[URL]`` to packages without hidden files or symlinks.
/// e.g. only actual package URLs
var validPackageURLs: [String]?
{
let items: [String]? = try? FileManager.default.contentsOfDirectory(atPath: path).filter { !$0.hasPrefix(".") }.filter
{ item in
/// Filter out all symlinks from the folder
let completeURLtoItem: URL = self.appendingPathComponent(item, conformingTo: .folder)
guard let isSymlink = completeURLtoItem.isSymlink()
else
{
return false
}
return !isSymlink
}
return items
}
/// Get URLs to a package's versions
var packageVersionURLs: [URL]?
{
AppConstants.shared.logger.debug("Will check URL \(self)")
do
{
let versions: [URL] = try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: [.isHiddenKey], options: .skipsHiddenFiles)
if versions.isEmpty
{
AppConstants.shared.logger.warning("Package URL \(self, privacy: .public) has no versions installed")
return nil
}
AppConstants.shared.logger.debug("URL \(self) has these versions: \(versions))")
return versions
}
catch
{
AppConstants.shared.logger.error("Failed while loading version for package \(lastPathComponent, privacy: .public) at URL \(self, privacy: .public)")
return nil
}
}
}
extension [URL]
{
/// Returns an array of versions from an array of URLs to available versions
var versions: [String]
{
return map
{ versionURL in
versionURL.lastPathComponent
}
}
}
// MARK: - Getting list of URLs in folder
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) throws -> [URL]
{
do
{
if let options
{
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
}
else
{
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
}
}
catch let folderReadingError
{
AppConstants.shared.logger.error("\(folderReadingError.localizedDescription)")
throw folderReadingError
}
}

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkModels
func deleteCachedDownloads() throws(CachedDownloadDeletionError)
{
@ -15,7 +16,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the symlinks, so we have do **delete ONLY THE SYMLINKS**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
for url in try AppConstants.shared.brewCachedFormulaeDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{
@ -47,7 +48,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the symlinks, so we have to **delete ONLY THE SYMLINKS**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
for url in try AppConstants.shared.brewCachedCasksDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{
@ -79,7 +80,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the downloads themselves, so we have do **DELETE EVERYTHING THAT IS NOT A SYMLINK**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
for url in try AppConstants.shared.brewCachedDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HealthCheckError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
/* enum CachePurgeError: Error
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum OrphanUninstallationError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HomebrewCachePurgeError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum OrphanRemovalError: LocalizedError
{

View File

@ -1,54 +0,0 @@
//
// Load up Installed Packages.swift
// Cork
//
// Created by David Bureš on 11.02.2023.
//
import CorkShared
import Foundation
/*
@MainActor
func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<BrewPackage>
{
AppConstants.shared.logger.info("Started \(whatToLoad == .formula ? "Formula" : "Cask", privacy: .public) loading task at \(Date(), privacy: .public)")
var contentsOfFolder: Set<BrewPackage> = .init()
do
{
switch whatToLoad
{
case .formula:
contentsOfFolder = try await getContentsOfFolder(targetFolder: AppConstants.shared.brewCellarPath)
case .cask:
contentsOfFolder = try await getContentsOfFolder(targetFolder: AppConstants.shared.brewCaskPath)
}
}
catch let packageLoadingError as PackageLoadingError
{
switch packageLoadingError
{
case .couldNotReadContentsOfParentFolder(let failureReason):
appState.showAlert(errorToShow: .couldNotGetContentsOfPackageFolder(failureReason))
case .failedWhileLoadingPackages:
appState.showAlert(errorToShow: .couldNotLoadAnyPackages(packageLoadingError))
case .failedWhileLoadingCertainPackage(let offendingPackage, let offendingPackageURL, let failureReason):
appState.showAlert(errorToShow: .couldNotLoadCertainPackage(offendingPackage, offendingPackageURL, failureReason: failureReason))
case .packageDoesNotHaveAnyVersionsInstalled(let offendingPackage):
appState.showAlert(errorToShow: .installedPackageHasNoVersions(corruptedPackageName: offendingPackage))
case .packageIsNotAFolder(let offendingFile, let offendingFileURL):
appState.showAlert(errorToShow: .installedPackageIsNotAFolder(itemName: offendingFile, itemURL: offendingFileURL))
}
}
catch
{
print("Something got completely fucked up while loading packages")
}
AppConstants.shared.logger.info("Finished \(whatToLoad == .formula ? "Formula" : "Cask", privacy: .public) loading task at \(Date(), privacy: .auto)")
return contentsOfFolder
}
*/

View File

@ -7,8 +7,10 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
import CorkModels
func searchForPackage(packageName: String, packageType: PackageType) async -> [String]
func searchForPackage(packageName: String, packageType: BrewPackage.PackageType) async -> [String]
{
var finalPackageArray: [String]

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HomebrewServiceLoadingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension ServicesTracker
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension ServicesTracker
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum ServiceStoppingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension HomebrewService
{

View File

@ -1,37 +0,0 @@
//
// Parse Tap Info.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
import Foundation
import CorkShared
func parseTapInfo(from rawJSON: String) async throws -> TapInfo?
{
let decoder: JSONDecoder = {
let decoder: JSONDecoder = .init()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
do
{
guard let jsonAsData: Data = rawJSON.data(using: .utf8, allowLossyConversion: false)
else
{
AppConstants.shared.logger.error("Could not convert tap JSON string into data")
throw JSONParsingError.couldNotConvertStringToData(failureReason: nil)
}
return try decoder.decode([TapInfo].self, from: jsonAsData).first
}
catch let decodingError
{
AppConstants.shared.logger.error("Failed while decoding tap info: \(decodingError.localizedDescription, privacy: .public)\n-\(decodingError, privacy: .public)")
throw JSONParsingError.couldNotDecode(failureReason: decodingError.localizedDescription)
}
}

View File

@ -8,6 +8,8 @@
import Foundation
import SwiftUI
import CorkShared
import CorkTerminalFunctions
import CorkModels
@MainActor
func refreshPackages(_ updateProgressTracker: UpdateProgressTracker, outdatedPackagesTracker: OutdatedPackagesTracker) async -> PackageUpdateAvailability

View File

@ -8,6 +8,7 @@
import Foundation
import SwiftUI
import CorkShared
import CorkTerminalFunctions
@MainActor
func updatePackages(updateProgressTracker: UpdateProgressTracker, detailStage: UpdatingProcessDetails) async

View File

@ -1,21 +0,0 @@
//
// Brew Tap.swift
// Cork
//
// Created by David Bureš on 10.02.2023.
//
import Foundation
struct BrewTap: Identifiable, Hashable
{
let id: UUID = .init()
let name: String
var isBeingModified: Bool = false
mutating func changeBeingModifiedStatus()
{
isBeingModified.toggle()
}
}

View File

@ -1,20 +0,0 @@
//
// Cached Download.swift
// Cork
//
// Created by David Bureš on 04.11.2023.
//
import Charts
import Foundation
import SwiftUI
struct CachedDownload: Identifiable, Hashable
{
var id: UUID = .init()
let packageName: String
let sizeInBytes: Int
var packageType: CachedDownloadType?
}

View File

@ -1,14 +0,0 @@
//
// Corrupted Package.swift
// Cork
//
// Created by David Bureš on 28.03.2024.
//
import Foundation
struct CorruptedPackage: Identifiable, Equatable
{
let id: UUID = .init()
let name: String
}

View File

@ -10,6 +10,7 @@ import SwiftUI
import CorkShared
import Defaults
import DefaultsMacros
import CorkModels
@Observable @MainActor
class TopPackagesTracker

View File

@ -1,60 +0,0 @@
//
// Minimal Homebrew Package.swift
// Cork
//
// Created by David Bureš on 25.05.2024.
//
import AppIntents
import Foundation
struct MinimalHomebrewPackage: Identifiable, Hashable, AppEntity, Codable
{
var id: UUID = .init()
var name: String
var type: PackageType
var installDate: Date?
var installedIntentionally: Bool
static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "intents.type.minimal-homebrew-package")
var displayRepresentation: DisplayRepresentation
{
DisplayRepresentation(
title: "\(name)",
subtitle: "intents.type.minimal-homebrew-package.representation.subtitle"
)
}
static let defaultQuery: MinimalHomebrewPackageIntentQuery = .init()
}
extension MinimalHomebrewPackage
{
init?(from homebrewPackage: BrewPackage?)
{
guard let homebrewPackage = homebrewPackage
else
{
return nil
}
self.init(
name: homebrewPackage.name,
type: homebrewPackage.type,
installedIntentionally: homebrewPackage.installedIntentionally
)
}
}
struct MinimalHomebrewPackageIntentQuery: EntityQuery
{
func entities(for _: [UUID]) async throws -> [MinimalHomebrewPackage]
{
return .init()
}
}

View File

@ -1,75 +0,0 @@
//
// Tap Codable Model.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
import Foundation
/// Decodable tap info
struct TapInfo: Codable
{
/// The name of the tap
let name: String
/// The user responsible for the tap
let user: String
/// Name of the upstream repo
let repo: String
/// Path to the tap
let path: URL
/// Whether the tap is currently added
let installed: Bool
/// Whether the tap is from the Homebrew developers
let official: Bool
// MARK: - The contents of the tap
/// The formulae included in the tap
let formulaNames: [String]
/// The casks included in the tap
let caskTokens: [String]
/// The paths to the formula files
let formulaFiles: [URL]?
/// The paths to the cask files
let caskFiles: [URL]?
/// No idea, honestly
let commandFiles: [String]?
/// Link to the actual repo
let remote: URL?
/// IDK
let customRemote: Bool?
var numberOfPackages: Int
{
return self.formulaNames.count + self.caskTokens.count
}
/// Formulae that include the package type. Useful for rpeviewing packages.
var includedFormulaeWithAdditionalMetadata: [MinimalHomebrewPackage]
{
return formulaNames.map
{ formulaName in
.init(name: formulaName, type: .formula, installedIntentionally: false)
}
}
var includedCasksWithAdditionalMetadata: [MinimalHomebrewPackage]
{
return caskTokens.map
{ caskName in
.init(name: caskName, type: .cask, installedIntentionally: false)
}
}
}

View File

@ -1,51 +0,0 @@
//
// Outdated Package.swift
// Cork
//
// Created by David Bureš on 05.04.2023.
//
import Foundation
struct OutdatedPackage: Identifiable, Equatable, Hashable
{
enum PackageUpdatingType
{
/// The package is updating through Homebrew
case homebrew
/// The package updates itself
case selfUpdating
var argument: String
{
switch self
{
case .homebrew:
return .init()
case .selfUpdating:
return "--greedy"
}
}
}
let id: UUID = .init()
let package: BrewPackage
let installedVersions: [String]
let newerVersion: String
var isMarkedForUpdating: Bool = true
var updatingManagedBy: PackageUpdatingType
static func == (lhs: OutdatedPackage, rhs: OutdatedPackage) -> Bool
{
return lhs.package.name == rhs.package.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(package.name)
}
}

View File

@ -7,11 +7,13 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
@Observable
class InstallationProgressTracker
{
var packageBeingInstalled: PackageInProgressOfBeingInstalled = .init(package: .init(name: "", type: .formula, installedOn: nil, versions: [], sizeInBytes: 0, downloadCount: nil), installationStage: .downloadingCask, packageInstallationProgress: 0)
var packageBeingInstalled: PackageInProgressOfBeingInstalled = .init(package: .init(name: "", type: .formula, installedOn: nil, versions: [], url: nil, sizeInBytes: 0, downloadCount: nil), installationStage: .downloadingCask, packageInstallationProgress: 0)
var numberOfPackageDependencies: Int = 0
var numberInLineOfPackageCurrentlyBeingFetched: Int = 0

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
struct RealTimeTerminalLine: Identifiable, Hashable, Equatable
{

View File

@ -1,80 +0,0 @@
//
// Brew Package Details.swift
// Cork
//
// Created by David Bureš on 18.07.2024.
//
import Foundation
import CorkShared
enum PinningUnpinningError: LocalizedError
{
case failedWhileChangingPinnedStatus
var errorDescription: String?
{
switch self
{
case .failedWhileChangingPinnedStatus:
return String(localized: "error.package-details.couldnt-pin-unpin")
}
}
}
@Observable @MainActor
class BrewPackageDetails
{
// MARK: - Immutable properties
/// Name of the package
let name: String
let description: String?
let homepage: URL
let tap: BrewTap
let installedAsDependency: Bool
let dependencies: [BrewPackageDependency]?
let outdated: Bool
let caveats: String?
let deprecated: Bool
let deprecationReason: String?
let isCompatible: Bool?
var dependents: [String]?
// MARK: - Init
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, deprecated: Bool, deprecationReason: String? = nil, isCompatible: Bool?)
{
self.name = name
self.description = description
self.homepage = homepage
self.tap = tap
self.installedAsDependency = installedAsDependency
self.dependents = dependents
self.dependencies = dependencies
self.outdated = outdated
self.deprecated = deprecated
self.deprecationReason = deprecationReason
self.caveats = caveats
self.isCompatible = isCompatible
}
// MARK: - Functions
func loadDependents() async
{
AppConstants.shared.logger.debug("Will load dependents for \(self.name)")
let packageDependentsRaw: String = await shell(AppConstants.shared.brewExecutablePath, ["uses", "--installed", name]).standardOutput
let finalDependents: [String] = packageDependentsRaw.components(separatedBy: "\n").dropLast()
AppConstants.shared.logger.debug("Dependents loaded: \(finalDependents)")
dependents = finalDependents
}
}

View File

@ -1,16 +0,0 @@
//
// Package Dependency.swift
// Cork
//
// Created by David Bureš on 27.02.2023.
//
import Foundation
struct BrewPackageDependency: Identifiable, Hashable
{
let id: UUID = .init()
let name: String
let version: String
let directlyDeclared: Bool
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
@Observable
class SearchResultTracker

View File

@ -1,20 +0,0 @@
//
// Terminal Output.swift
// Cork
//
// Created by David Bureš on 12.02.2023.
//
import Foundation
struct TerminalOutput
{
var standardOutput: String
var standardError: String
}
enum StreamedTerminalOutput
{
case standardOutput(String)
case standardError(String)
}

View File

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import CorkModels
protocol DismissablePane: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct BrewfileImportProgressView: View
{

View File

@ -10,6 +10,7 @@ import CorkShared
import SwiftUI
import ButtonKit
import Defaults
import CorkModels
struct AddFormulaView: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct SearchResultRow: View, Sendable
{
@ -126,11 +127,11 @@ struct SearchResultRow: View, Sendable
do
{
let searchedForPackage: BrewPackage = .init(name: searchedForPackage.name, type: searchedForPackage.type, installedOn: Date(), versions: [], sizeInBytes: nil, downloadCount: nil)
let searchedForPackage: BrewPackage = .init(name: searchedForPackage.name, type: searchedForPackage.type, installedOn: Date(), versions: [], url: nil, sizeInBytes: nil, downloadCount: nil)
do
{
let parsedPackageInfo: BrewPackageDetails = try await searchedForPackage.loadDetails()
let parsedPackageInfo: BrewPackage.BrewPackageDetails = try await searchedForPackage.loadDetails()
description = parsedPackageInfo.description

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct InstallationInitialView: View
{
@ -116,6 +117,7 @@ struct InstallationInitialView: View
))
}
.disabled(foundPackageSelection == nil)
.labelStyle(.titleOnly)
}
@ViewBuilder

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct TopPackagesSection: View
{
@ -15,7 +16,7 @@ struct TopPackagesSection: View
let packageTracker: TopPackagesTracker
let trackerType: PackageType
let trackerType: BrewPackage.PackageType
private var packages: [BrewPackage]
{

View File

@ -7,6 +7,8 @@
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct InstallingPackageView: View
{

View File

@ -7,6 +7,7 @@
import CorkShared
import SwiftUI
import CorkModels
struct PresentingSearchResultsView: View
{
@ -129,6 +130,7 @@ struct PresentingSearchResultsView: View
AppConstants.shared.logger.debug("Would preview package \(selectedPackage.name)")
}
.disabled(foundPackageSelection == nil)
.labelStyle(.titleOnly)
}
@ViewBuilder
@ -185,7 +187,7 @@ struct PresentingSearchResultsView: View
private struct SearchResultsSection: View
{
let sectionType: PackageType
let sectionType: BrewPackage.PackageType
let packageList: [BrewPackage]

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct InstallationSearchingView: View, Sendable
{
@ -28,11 +29,11 @@ struct InstallationSearchingView: View, Sendable
for formula in await foundFormulae
{
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil, downloadCount: nil))
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], url: nil, sizeInBytes: nil, downloadCount: nil))
}
for cask in await foundCasks
{
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil, downloadCount: nil))
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], url: nil, sizeInBytes: nil, downloadCount: nil))
}
packageInstallationProcessStep = .presentingSearchResults

View File

@ -7,6 +7,8 @@
import CorkShared
import SwiftUI
import CorkModels
import CorkTerminalFunctions
struct AdoptingAlreadyInstalledCaskView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct AnotherProcessAlreadyRunningView: View
{

View File

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import CorkModels
struct BinaryAlreadyExistsView: View, Sendable
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct InstallationFatalErrorView: View
{

View File

@ -9,6 +9,7 @@ import CorkNotifications
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct InstallationFinishedSuccessfullyView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct SudoRequiredView: View, Sendable
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct WrongArchitectureView: View, Sendable
{

View File

@ -8,6 +8,7 @@
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct LicensingView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct Licensing_BoughtView: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct Licensing_DemoView: View
{

View File

@ -9,6 +9,7 @@ import SwiftUI
import CorkShared
import ButtonKit
import Defaults
import CorkModels
struct Licensing_NotBoughtOrActivatedView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct Licensing_SelfCompiledView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
enum MaintenanceSteps
{

View File

@ -8,6 +8,7 @@
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct MaintenanceFinishedView: View
{

View File

@ -7,6 +7,8 @@
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct MaintenanceRunningView: View
{

View File

@ -0,0 +1,44 @@
//
// Adoption Results List.swift
// Cork
//
// Created by David Bureš - P on 08.10.2025.
//
import SwiftUI
struct AdoptionResultsList: View
{
@Environment(\.openWindow) var openWindow: OpenWindowAction
@Environment(MassAppAdoptionView.MassAppAdoptionTacker.self) var massAppAdoptionTracker: MassAppAdoptionView.MassAppAdoptionTacker
var body: some View
{
DisclosureGroup("mass-adoption.failed.details-dropdown.label")
{
List(massAppAdoptionTracker.unsuccessfullyAdoptedApps)
{ unsuccessfullyAdoptedApp in
if case .failedWithError(let failedAdoptionCandidate, let error) = unsuccessfullyAdoptedApp
{
HStack(alignment: .center)
{
Text(failedAdoptionCandidate.caskName)
Spacer()
Button {
openWindow(id: .errorInspectorWindowID, value: error)
} label: {
Label("action.inspect-error", systemImage: "info.circle")
}
.labelStyle(.iconOnly)
}
}
}
.listStyle(.bordered(alternatesRowBackgrounds: true))
.frame(minHeight: 100)
}
}
}

View File

@ -0,0 +1,64 @@
//
// Mass Adoption Stage - Ready.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import CorkShared
import SwiftUI
import CorkModels
struct MassAdoptionStage_Adopting: View
{
@Environment(MassAppAdoptionView.MassAppAdoptionTacker.self) var massAppAdoptionTracker: MassAppAdoptionView.MassAppAdoptionTacker
let appsToAdopt: [BrewPackagesTracker.AdoptableApp]
@State private var currentAdoptionIndex: Double = 0
var body: some View
{
ProgressView(value: currentAdoptionIndex, total: Double(appsToAdopt.count))
{
Text("app-adoption.currently-being-adopted.\(massAppAdoptionTracker.appCurrentlyBeingAdopted.appExecutable)")
}
.progressViewStyle(.linear)
.task
{
for appToAdopt in appsToAdopt
{
AppConstants.shared.logger.info("Will start adoption process for \(appToAdopt.caskName)")
await massAppAdoptionTracker.adoptNextApp(appToAdopt: appToAdopt)
currentAdoptionIndex += 1
}
if massAppAdoptionTracker.unsuccessfullyAdoptedApps.isEmpty
{
AppConstants.shared.logger.info("All selected apps were adopted successfully!")
massAppAdoptionTracker.massAdoptionStage = .finished(result: .success)
}
else if !massAppAdoptionTracker.unsuccessfullyAdoptedApps.isEmpty && !massAppAdoptionTracker.successfullyAdoptedApps.isEmpty
{
AppConstants.shared.logger.warning("Some selected apps were adoptes successfully, some unsuccessfully")
massAppAdoptionTracker.massAdoptionStage = .finished(result: .someSuccessSomeFailure)
}
else
{
AppConstants.shared.logger.error("No selected apps were adopted successfully")
massAppAdoptionTracker.massAdoptionStage = .finished(result: .failure)
}
}
.onDisappear
{
AppConstants.shared.logger.info("Cancelled the app adoption - will also cancel the app adoption process")
massAppAdoptionTracker.cancel()
}
}
}

View File

@ -0,0 +1,32 @@
//
// Mass Adoption Stage - Failure.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import SwiftUI
struct MassAdoptionStage_Failure: View
{
@Environment(\.openWindow) var openWindow: OpenWindowAction
@Environment(MassAppAdoptionView.MassAppAdoptionTacker.self) var massAppAdoptionTracker: MassAppAdoptionView.MassAppAdoptionTacker
var body: some View
{
ComplexWithIcon(systemName: "seal")
{
VStack(alignment: .leading, spacing: 10)
{
HeadlineWithSubheadline(
headline: "mass-adoption.some-failed",
subheadline: "mass-adoption.some-failed.message",
alignment: .leading
)
AdoptionResultsList()
}
}
}
}

View File

@ -0,0 +1,30 @@
//
// Mass Adoption Stage - Some Success Some Failure.swift
// Cork
//
// Created by David Bureš - P on 08.10.2025.
//
import SwiftUI
struct MassAdoptionStage_SomeSuccessSomeFailure: View
{
@Environment(MassAppAdoptionView.MassAppAdoptionTacker.self) var massAppAdoptionTracker: MassAppAdoptionView.MassAppAdoptionTacker
var body: some View
{
ComplexWithIcon(systemName: "xmark.seal")
{
VStack(alignment: .leading, spacing: 10)
{
HeadlineWithSubheadline(
headline: "mass-adoption.failed",
subheadline: "mass-adoption.failed.message",
alignment: .leading
)
AdoptionResultsList()
}
}
}
}

View File

@ -0,0 +1,39 @@
//
// Mass Adoption Stage - Success.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import SwiftUI
import Defaults
import CorkNotifications
struct MassAdoptionStage_Success: View
{
@Default(.notifyAboutMassAdoptionResults) var notifyAboutMassAdoptionResults: Bool
var body: some View
{
DisappearableSheet
{
ComplexWithIcon(systemName: "checkmark.seal")
{
HeadlineWithSubheadline(
headline: "mass-adoption.finished",
subheadline: "mass-adoption.finished.description",
alignment: .leading
)
}
}
.onAppear {
if notifyAboutMassAdoptionResults
{
sendNotification(
title: "mass-adoption.finished",
body: "mass-adoption.finished-successfully.message"
)
}
}
}
}

View File

@ -0,0 +1,182 @@
//
// Mass App Adoption View.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import SwiftUI
import CorkModels
typealias AdoptionProcessResult = Result<BrewPackagesTracker.AdoptableApp, MassAppAdoptionView.AdoptionAttemptFailure>
struct MassAppAdoptionView: View
{
@Observable
final class MassAppAdoptionTacker
{
@ObservationIgnored
var adoptionProcess: Process?
var outputLines: [RealTimeTerminalLine] = .init()
var massAdoptionStage: MassAdoptionStage = .adopting
private(set) var appCurrentlyBeingAdopted: BrewPackagesTracker.AdoptableApp
private(set) var currentAdoptionIndex: Int
private(set) var appAdoptionResults: [AdoptionProcessResult] = .init()
var successfullyAdoptedApps: [BrewPackagesTracker.AdoptableApp]
{
return appAdoptionResults.compactMap
{ rawResult in
if case .success(let success) = rawResult {
return success
}
else
{
return nil
}
}
}
var unsuccessfullyAdoptedApps: [MassAppAdoptionView.AdoptionAttemptFailure]
{
return appAdoptionResults.compactMap
{ rawResult in
if case .failure(let failure) = rawResult {
return failure
}
else
{
return nil
}
}
}
init(appsToAdopt: [BrewPackagesTracker.AdoptableApp])
{
self.appCurrentlyBeingAdopted = appsToAdopt.first!
self.currentAdoptionIndex = 0
}
deinit
{
cancel()
}
@MainActor
func adoptNextApp(appToAdopt: BrewPackagesTracker.AdoptableApp) async
{
self.appCurrentlyBeingAdopted = appToAdopt
self.currentAdoptionIndex += 1
self.appAdoptionResults.append(await self.adoptApp(appToAdopt))
}
@discardableResult
func cancel() -> Bool
{
guard let adoptionProcess else { return false }
adoptionProcess.terminate()
self.adoptionProcess = nil
return true
}
}
@Environment(\.dismiss) var dismiss: DismissAction
@Environment(BrewPackagesTracker.self) var brewPackagesTracker: BrewPackagesTracker
@Environment(CachedDownloadsTracker.self) var cachedDownloadsTracker: CachedDownloadsTracker
let appsToAdopt: [BrewPackagesTracker.AdoptableApp]
@State private var massAdoptionTracker: MassAppAdoptionTacker
init(appsToAdopt: [BrewPackagesTracker.AdoptableApp])
{
self.appsToAdopt = appsToAdopt
self.massAdoptionTracker = .init(appsToAdopt: appsToAdopt)
}
enum AdoptionAttemptFailure: Identifiable, Error
{
case failedWithError(failedAdoptionCandidate: BrewPackagesTracker.AdoptableApp, error: String)
var id: UUID
{
switch self {
case .failedWithError(let failedAdoptionCandidate, let error):
return failedAdoptionCandidate.id
}
}
}
enum MassAdoptionStage
{
case adopting, finished(result: AdoptionResult)
enum AdoptionResult
{
case success
case someSuccessSomeFailure
case failure
}
var isDismissable: Bool
{
switch self
{
case .adopting:
return true
case .finished:
return true
}
}
}
var body: some View
{
NavigationStack
{
SheetTemplate(isShowingTitle: true)
{
switch massAdoptionTracker.massAdoptionStage
{
case .adopting:
MassAdoptionStage_Adopting(appsToAdopt: appsToAdopt)
case .finished(let result):
switch result
{
case .success:
MassAdoptionStage_Success()
case .someSuccessSomeFailure:
MassAdoptionStage_SomeSuccessSomeFailure()
case .failure:
MassAdoptionStage_Failure()
}
}
}
.navigationTitle("mass-adoption.title")
.toolbar
{
if massAdoptionTracker.massAdoptionStage.isDismissable
{
ToolbarItem(placement: .cancellationAction)
{
DismissSheetButton(dismiss: _dismiss)
}
}
}
}
.environment(massAdoptionTracker)
.onDisappear
{
Task
{
try? await brewPackagesTracker.synchronizeInstalledPackages(cachedDownloadsTracker: cachedDownloadsTracker)
}
}
}
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBarItem: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import CorkNotifications
import CorkModels
struct MenuBar_CachedDownloadsCleanup: View
{

View File

@ -9,6 +9,7 @@ import ButtonKit
import CorkNotifications
import CorkShared
import SwiftUI
import CorkModels
struct MenuBar_OrphanCleanup: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageInstallation: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageOverview: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageUpdating: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackagePreview: View
{

View File

@ -7,6 +7,8 @@
import CorkShared
import SwiftUI
import CorkModels
import ApplicationInspector
struct PackageDetailView: View, Sendable, DismissablePane
{
@ -39,7 +41,9 @@ struct PackageDetailView: View, Sendable, DismissablePane
var isInPreviewWindow: Bool = false
@State private var packageDetails: BrewPackageDetails? = nil
@State private var packageDetails: BrewPackage.BrewPackageDetails? = nil
@State private var caskExecutable: Application? = nil
@Environment(BrewPackagesTracker.self) var brewPackagesTracker: BrewPackagesTracker
@ -92,9 +96,15 @@ struct PackageDetailView: View, Sendable, DismissablePane
isShowingExpandedCaveats: $isShowingExpandedCaveats
)
PackageDependencies(dependencies: packageDetails?.dependencies, isDependencyDisclosureGroupExpanded: $isShowingExpandedDependencies)
PackageDependencies(
dependencies: packageDetails?.dependencies,
isDependencyDisclosureGroupExpanded: $isShowingExpandedDependencies
)
PackageSystemInfo(package: packageStructureToUse)
PackageSystemInfo(
package: packageStructureToUse,
caskExecutable: caskExecutable
)
}
}
}
@ -157,6 +167,19 @@ struct PackageDetailView: View, Sendable, DismissablePane
erroredOut = (true, packageInfoDecodingError.localizedDescription)
}
}
.task(id: package.id)
{ // For casks, try to load the application executable
if package.type == .cask
{
AppConstants.shared.logger.info("Package is cask, will see what the app's location is for url \(package.url as NSObject?)")
if let packageURL = package.url
{
AppConstants.shared.logger.info("Will try to load app icon for URL \(packageURL)")
caskExecutable = try? .init(from: packageURL)
}
}
}
}
}
@ -217,10 +240,10 @@ private extension BrewPackagesTracker
struct FastPackageComparableRepresentation: Hashable
{
let name: String
let type: PackageType
let type: BrewPackage.PackageType
let versions: [String]
init(name: String, type: PackageType, versions: [String])
init(name: String, type: BrewPackage.PackageType, versions: [String])
{
self.name = name
self.type = type

View File

@ -8,13 +8,14 @@
import CorkShared
import Defaults
import SwiftUI
import CorkModels
struct BasicPackageInfoView: View
{
@Default(.caveatDisplayOptions) var caveatDisplayOptions: PackageCaveatDisplay
let package: BrewPackage
let packageDetails: BrewPackageDetails
let packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct DependencyList: View
{
@ -43,7 +44,7 @@ struct DependencyList: View
{
TableColumn("package-details.dependencies.results.name")
{ dependency in
SanitizedPackageName(package: .init(name: dependency.name, type: .formula, installedOn: nil, versions: [dependency.version], sizeInBytes: nil, downloadCount: nil), shouldShowVersion: false)
SanitizedPackageName(package: .init(name: dependency.name, type: .formula, installedOn: nil, versions: [dependency.version], url: nil, sizeInBytes: nil, downloadCount: nil), shouldShowVersion: false)
}
TableColumn("package-details.dependencies.results.version")
{ dependency in

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackageDependencies: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import CorkShared
import CorkModels
struct PackageDetailHeaderComplex: View
{
@ -22,7 +23,7 @@ struct PackageDetailHeaderComplex: View
var isInPreviewWindow: Bool
@Bindable var packageDetails: BrewPackageDetails
@Bindable var packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool

View File

@ -9,6 +9,7 @@ import SwiftUI
import CorkShared
import ButtonKit
import Defaults
import CorkModels
struct PackageModificationButtons: View
{
@ -21,7 +22,7 @@ struct PackageModificationButtons: View
@Environment(OutdatedPackagesTracker.self) var outdatedPackagesTracker: OutdatedPackagesTracker
let package: BrewPackage
@Bindable var packageDetails: BrewPackageDetails
@Bindable var packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool
@ -36,6 +37,7 @@ struct PackageModificationButtons: View
if package.type == .formula
{
PinUnpinButton(package: package)
.labelStyle(.titleOnly)
}
Spacer()
@ -52,12 +54,14 @@ struct PackageModificationButtons: View
if !allowMoreCompleteUninstallations
{
UninstallPackageButton(package: package)
.labelStyle(.titleOnly)
}
else
{
Menu
{
PurgePackageButton(package: package)
.labelStyle(.titleOnly)
} label: {
Text("action.uninstall-\(package.name)")
} primaryAction: {

View File

@ -7,6 +7,7 @@
import SwiftUI
import ButtonKit
import CorkModels
struct PinUnpinButton: View
{
@ -23,7 +24,9 @@ struct PinUnpinButton: View
{
await package.performPinnedStatusChangeAction(appState: appState, brewPackagesTracker: brewPackagesTracker)
} label: {
Text(package.isPinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
let labelText: LocalizedStringKey = package.isPinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))"
Label(labelText, systemImage: "pin.fill")
}
.asyncButtonStyle(.leading)
.disabledWhenLoading()

View File

@ -6,10 +6,14 @@
//
import SwiftUI
import ApplicationInspector
import CorkModels
struct PackageSystemInfo: View
{
let package: BrewPackage
let caskExecutable: Application?
@State private var isShowingCaskSizeHelpPopover: Bool = false
@ -19,47 +23,57 @@ struct PackageSystemInfo: View
{
Section
{
LabeledContent
{
Text(installedOnDate.formatted(.packageInstallationStyle))
} label: {
Text("package-details.install-date")
}
caskInstalledAsLine
installedOnDateLine(installedOnDate)
if let packageSize = package.sizeInBytes
{
LabeledContent
{
HStack
{
Text(packageSize.formatted(.byteCount(style: .file)))
if package.type == .cask
{
HelpButton
{
isShowingCaskSizeHelpPopover.toggle()
}
.help("package-details.size.help")
.popover(isPresented: $isShowingCaskSizeHelpPopover)
{
VStack(alignment: .leading, spacing: 10)
{
Text("package-details.size.help")
.font(.headline)
Text("package-details.size.help.body-1")
Text("package-details.size.help.body-2")
}
.multilineTextAlignment(.leading)
.padding()
.frame(width: 300)
}
}
}
} label: {
Text("package-details.size")
}
}
packageSizeLine(package.sizeInBytes)
}
}
}
@ViewBuilder
var caskInstalledAsLine: some View
{
if let caskExecutable
{
LabeledContent
{
AppIconDisplay(
displayType: .asIconWithAppNameDisplayed(
usingApp: caskExecutable,
namePosition: .besideAppIcon
),
allowRevealingInFinderFromIcon: true
)
} label: {
Text("package-details.installed-as")
}
}
}
@ViewBuilder
func installedOnDateLine(_ installedOnDate: Date) -> some View
{
LabeledContent
{
Text(installedOnDate.formatted(.packageInstallationStyle))
} label: {
Text("package-details.install-date")
}
}
@ViewBuilder
func packageSizeLine(_ packageSize: Int64?) -> some View
{
if let packageSize = package.sizeInBytes
{
LabeledContent
{
Text(packageSize.formatted(.byteCount(style: .file)))
} label: {
Text("package-details.size")
}
}
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackageListItem: View
{

View File

@ -8,6 +8,8 @@
import Foundation
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct ReinstallCorruptedPackageView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import ButtonKit
import CorkModels
struct SudoRequiredForRemovalSheet: View, Sendable
{

View File

@ -0,0 +1,134 @@
//
// App Icon Display.swift
// Cork
//
// Created by David Bureš - P on 22.10.2025.
//
import SwiftUI
import ApplicationInspector
/// Show the icon of a linked app
struct AppIconDisplay: View
{
enum DisplayType
{
case asIcon(
usingApp: Application
)
case asIconWithAppNameDisplayed(
usingApp: Application,
namePosition: AppNamePosition
)
case asPathControl(
usingURL: URL
)
enum AppNamePosition
{
case besideAppIcon
case underAppIcon
}
}
let displayType: DisplayType
let allowRevealingInFinderFromIcon: Bool
var body: some View
{
switch displayType
{
case .asIcon(let usingApp):
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
case .asIconWithAppNameDisplayed(let usingApp, let namePosition):
switch namePosition
{
case .besideAppIcon:
HStack(alignment: .center, spacing: 5)
{
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
applicationName(app: usingApp)
}
case .underAppIcon:
VStack(alignment: .center, spacing: 5)
{
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
applicationName(app: usingApp)
}
}
case .asPathControl(let usingURL):
AppIconDisplay_AsPathControl(urlToApp: usingURL)
}
}
@ViewBuilder
func applicationName(app: Application) -> some View
{
Text(app.name)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
private struct ApplicationIconImage: View
{
let app: Application
let allowRevealingInFinderThroughIcon: Bool
var body: some View
{
if let appIconImage = app.iconImage
{
appIconImage
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35)
.contextMenu {
Button
{
app.url.revealInFinder(.openParentDirectoryAndHighlightTarget)
} label: {
Label("action.reveal-\(app.name)-in-finder", systemImage: "finder")
}
}
}
}
}
private struct AppIconDisplay_AsPathControl: NSViewRepresentable
{
typealias NSViewType = NSPathControl
let urlToApp: URL
func makeNSView(context _: Context) -> NSPathControl
{
let pathControl: NSPathControl = .init()
pathControl.url = urlToApp
if let lastPathItem = pathControl.pathItems.last
{
pathControl.pathItems = [lastPathItem]
}
return pathControl
}
func updateNSView(_: NSPathControl, context _: Context)
{}
}

Some files were not shown because too many files have changed in this diff Show More