Merge branch 'main' into DisconnectSwipe

This commit is contained in:
Benjamin Faershtein 2025-04-26 20:55:36 -07:00 committed by GitHub
commit a1fcb30394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 12540 additions and 4975 deletions

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@
BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; };
BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; };
BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; };
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; };
BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; };
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; };
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; };
@ -177,8 +178,6 @@
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */; };
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A0E2A05920E006ED576 /* FileManager.swift */; };
DDB75A112A059258006ED576 /* Url.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A102A059258006ED576 /* Url.swift */; };
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; };
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; };
@ -328,6 +327,7 @@
BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = "<group>"; };
BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = "<group>"; };
BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = "<group>"; };
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = "<group>"; };
BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = "<group>"; };
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = "<group>"; };
@ -477,8 +477,6 @@
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = "<group>"; };
DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = "<group>"; };
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
@ -921,15 +919,6 @@
path = Map;
sourceTree = "<group>";
};
DDB75A122A0593CD006ED576 /* Map */ = {
isa = PBXGroup;
children = (
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
);
path = Map;
sourceTree = "<group>";
};
DDC2E14B26CE248E0042C5E4 = {
isa = PBXGroup;
children = (
@ -1067,7 +1056,6 @@
isa = PBXGroup;
children = (
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
DDB75A122A0593CD006ED576 /* Map */,
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
@ -1124,6 +1112,7 @@
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
251926882C3BAF2E00249DF5 /* Actions */,
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1281,10 +1270,9 @@
pl,
he,
fr,
"zh-Hant-TW",
se,
"pt-PT",
sr,
it,
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
@ -1450,7 +1438,6 @@
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */,
DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */,
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */,
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */,
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
@ -1525,7 +1512,6 @@
DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */,
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */,
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
@ -1566,6 +1552,7 @@
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */,
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */,
2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */,
@ -1809,7 +1796,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.5.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1843,7 +1830,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.5.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1875,7 +1862,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.5.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1908,7 +1895,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.5.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "thinknode_m1.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,109 @@
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
<defs id="defs1">
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
</defs>
<g id="Layer_3" data-name="Layer 3">
<path class="cls-1" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
<rect class="cls-2" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
<rect class="cls-1" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
<rect class="cls-3" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
<path class="cls-1" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
<rect class="cls-2" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
<polygon class="cls-1" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
<rect class="cls-1" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
<path class="cls-2" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
<rect class="cls-2" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
<polygon class="cls-1" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
<rect class="cls-2" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
<path class="cls-2" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
<path class="cls-2" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
<path class="cls-4" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
<rect class="cls-2" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
<circle class="cls-5" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
<circle class="cls-1" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
<circle class="cls-1" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
<circle class="cls-1" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
<circle class="cls-1" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
<circle class="cls-1" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
<path class="cls-4" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
<path class="cls-4" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
<path class="cls-6" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
<path class="cls-6" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
<path class="cls-6" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
<path class="cls-6" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
<path class="cls-6" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
<path class="cls-6" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
<path class="cls-1" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
<path class="cls-1" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
<path class="cls-1" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
<path class="cls-1" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
<path class="cls-1" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
<path class="cls-1" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
<path class="cls-1" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
<path class="cls-1" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
<path class="cls-1" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
<path class="cls-1" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
<path class="cls-1" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
<path class="cls-1" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
<path class="cls-1" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
<path class="cls-1" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
</g>
<g id="Layer_2" data-name="Layer 2">
<path class="cls-8" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
<line class="cls-8" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
<line class="cls-8" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
<polyline class="cls-8" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
<line class="cls-8" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
<path class="cls-8" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
<path class="cls-8" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
<line class="cls-8" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
<line class="cls-8" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
<rect class="cls-8" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
<line class="cls-8" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
<line class="cls-8" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
<path class="cls-8" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
<line class="cls-8" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
<path class="cls-8" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
<rect class="cls-8" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
<rect class="cls-8" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
<rect class="cls-8" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
<path class="cls-8" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
<path class="cls-8" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
<path class="cls-8" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
<path class="cls-8" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
<path class="cls-8" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
<path class="cls-8" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
<rect class="cls-8" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
<path class="cls-8" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
<path class="cls-8" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
<path class="cls-8" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
<rect class="cls-8" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
<circle class="cls-8" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
<circle class="cls-8" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
<circle class="cls-8" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
<circle class="cls-8" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
<circle class="cls-8" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
<circle class="cls-8" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
<line class="cls-8" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
<line class="cls-8" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
<line class="cls-8" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
<line class="cls-8" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
<line class="cls-8" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
<line class="cls-8" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
<line class="cls-8" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
<line class="cls-8" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
<line class="cls-8" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
<line class="cls-8" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
<line class="cls-8" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
<line class="cls-8" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
<line class="cls-8" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
<line class="cls-8" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
<line class="cls-8" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
<line class="cls-8" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
<line class="cls-8" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
<line class="cls-8" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
<path class="cls-8" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
<path class="cls-8" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "thinknode_m2.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg75"
sodipodi:docname="thinknode_m2.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
viewBox="388.5 121.73 413.05 787.86"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview75"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.7680002"
inkscape:cx="265.65816"
inkscape:cy="681.92672"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_3" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#262626;}.cls-2{fill:#353535;}.cls-3{fill:#303030;}.cls-4{fill:#f05043;}.cls-5,.cls-6,.cls-7,.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-6{stroke-width:0.88px;}.cls-7{stroke-width:0.95px;}.cls-8{stroke-width:1px;}.cls-9{fill:#acdee5;}</style>
</defs>
<g
id="Layer_3"
data-name="Layer 3">
<polygon
class="cls-1"
points="575.63 361.13 575.34 354.09 574.7 338.7 574.41 331.65 479.53 331.65 479.23 338.7 478.57 354.09 478.26 361.13 575.63 361.13"
id="polygon1" />
<polyline
class="cls-2"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline1" />
<path
class="cls-2"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path1" />
<path
class="cls-2"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path2" />
<path
class="cls-2"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path3" />
<polyline
class="cls-2"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline3" />
<rect
class="cls-1"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect3" />
<rect
class="cls-2"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect4" />
<path
class="cls-3"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path4" />
<path
class="cls-1"
d="M716.86,532.78H462.77a18,18,0,0,0-18,18V673.91a18,18,0,0,0,18,18H716.86a18,18,0,0,0,18-18V550.78A18,18,0,0,0,716.86,532.78Zm6.52,137.48a10.7,10.7,0,0,1-10.7,10.7H465.38a10.7,10.7,0,0,1-10.7-10.7V551.83a10.7,10.7,0,0,1,10.7-10.7h247.3a10.7,10.7,0,0,1,10.7,10.7Z"
id="path5" />
<rect
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect5" />
<path
class="cls-3"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path6" />
<path
class="cls-3"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path7" />
<path
class="cls-3"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path8" />
<path
class="cls-3"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path9" />
<rect
class="cls-1"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect9" />
<circle
class="cls-4"
cx="515.99"
cy="794.33"
r="5.48"
id="circle9" />
<circle
class="cls-2"
cx="637.4"
cy="794.33"
r="5.48"
id="circle10" />
<circle
class="cls-2"
cx="660.08"
cy="794.33"
r="5.48"
id="circle11" />
<circle
class="cls-2"
cx="539.09"
cy="794.33"
r="5.48"
id="circle12" />
<circle
class="cls-2"
cx="564.93"
cy="794.33"
r="5.48"
id="circle13" />
<circle
class="cls-2"
cx="590.77"
cy="794.33"
r="5.48"
id="circle14" />
<circle
class="cls-2"
cx="614.09"
cy="794.33"
r="5.48"
id="circle15" />
<path
class="cls-1"
d="M475.77,856.71h-3.41V832.6h-8.47v-3H484.2v3h-8.43Z"
id="path15" />
<path
class="cls-1"
d="M491.19,836.32c0,.48,0,1-.06,1.48s-.08,1-.13,1.37h.23a5.69,5.69,0,0,1,1.63-1.75,7.82,7.82,0,0,1,2.2-1,8.81,8.81,0,0,1,2.51-.36,9.64,9.64,0,0,1,4.12.78,5.2,5.2,0,0,1,2.49,2.41,9.43,9.43,0,0,1,.83,4.25v13.25h-3.3v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21,6.41,6.41,0,0,0-3.57.85,4.68,4.68,0,0,0-1.84,2.51,12.81,12.81,0,0,0-.55,4v10.52h-3.34V827.85h3.34Z"
id="path16" />
<path
class="cls-1"
d="M513,828.73a2,2,0,0,1,1.35.51,2,2,0,0,1,.59,1.61,2.07,2.07,0,0,1-.59,1.6A2,2,0,0,1,513,833a2,2,0,0,1-1.4-.53,2.1,2.1,0,0,1-.57-1.6,2.06,2.06,0,0,1,.57-1.61A2,2,0,0,1,513,828.73Zm1.64,7.63v20.35h-3.35V836.36Z"
id="path17" />
<path
class="cls-1"
d="M530.91,836a7.69,7.69,0,0,1,5.5,1.77c1.24,1.17,1.86,3.08,1.86,5.71v13.25H535v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21q-3.37,0-4.67,1.9a9.7,9.7,0,0,0-1.29,5.47v10.55h-3.34V836.36h2.7l.49,2.77h.19a6.17,6.17,0,0,1,1.69-1.76,7.31,7.31,0,0,1,2.22-1A9.16,9.16,0,0,1,530.91,836Z"
id="path18" />
<path
class="cls-1"
d="M547.88,842.93q0,.6-.06,1.59t-.09,1.71h.15l.68-.87.93-1.16c.32-.39.59-.72.82-1l6.49-6.87h3.91l-8.24,8.69,8.81,11.66h-4l-7.06-9.49-2.32,2v7.48h-3.3V827.85h3.3Z"
id="path19" />
<path
class="cls-1"
d="M586.8,856.71h-4l-14.5-22.52h-.15c.05.59.09,1.28.13,2.07s.07,1.65.11,2.55.06,1.81.06,2.75v15.15h-3.15V829.6h4L583.72,852h.16c0-.4-.06-1-.1-1.82s-.08-1.7-.11-2.66-.06-1.85-.06-2.66V829.6h3.19Z"
id="path20" />
<path
class="cls-1"
d="M611.4,846.5a14.11,14.11,0,0,1-.66,4.5,9.41,9.41,0,0,1-1.9,3.32,7.92,7.92,0,0,1-3,2.07,10.53,10.53,0,0,1-3.93.7,9.66,9.66,0,0,1-3.72-.7,8.18,8.18,0,0,1-3-2.07,9.67,9.67,0,0,1-2-3.32,13.29,13.29,0,0,1-.7-4.5,13,13,0,0,1,1.14-5.72,8.25,8.25,0,0,1,3.26-3.57A10,10,0,0,1,602,836a9.47,9.47,0,0,1,4.87,1.23,8.61,8.61,0,0,1,3.31,3.57A12.41,12.41,0,0,1,611.4,846.5Zm-15.37,0a12.33,12.33,0,0,0,.62,4.15,5.45,5.45,0,0,0,2,2.72,6.49,6.49,0,0,0,6.76,0,5.5,5.5,0,0,0,2-2.72,12.32,12.32,0,0,0,.63-4.15,11.76,11.76,0,0,0-.65-4.14,5.53,5.53,0,0,0-1.95-2.64,5.76,5.76,0,0,0-3.4-.93,5.08,5.08,0,0,0-4.52,2.05A9.87,9.87,0,0,0,596,846.5Z"
id="path21" />
<path
class="cls-1"
d="M623.9,857.09a7.62,7.62,0,0,1-6.08-2.64c-1.52-1.76-2.28-4.38-2.28-7.88s.77-6.13,2.3-7.91a7.61,7.61,0,0,1,6.09-2.68,8.58,8.58,0,0,1,2.78.4,6.79,6.79,0,0,1,2,1.08,7.87,7.87,0,0,1,1.48,1.52h.22c0-.33-.07-.82-.13-1.46s-.09-1.16-.09-1.54v-8.13h3.34v28.86h-2.7l-.49-2.73h-.15a7.83,7.83,0,0,1-1.48,1.57,6.86,6.86,0,0,1-2.07,1.12A8.44,8.44,0,0,1,623.9,857.09Zm.53-2.77q3.23,0,4.53-1.77a8.85,8.85,0,0,0,1.31-5.33v-.61a11.15,11.15,0,0,0-1.25-5.83c-.83-1.35-2.38-2-4.63-2a4.45,4.45,0,0,0-4,2.15,10.68,10.68,0,0,0-1.35,5.75,10.12,10.12,0,0,0,1.35,5.66A4.56,4.56,0,0,0,624.43,854.32Z"
id="path22" />
<path
class="cls-1"
d="M647.89,836a8.5,8.5,0,0,1,4.5,1.14,7.45,7.45,0,0,1,2.89,3.21,11,11,0,0,1,1,4.84v2H642.35a7.71,7.71,0,0,0,1.76,5.26,6.19,6.19,0,0,0,4.73,1.8,14.83,14.83,0,0,0,3.44-.36,19.71,19.71,0,0,0,3.09-1v2.92a16,16,0,0,1-3.07,1,18.05,18.05,0,0,1-3.61.32,10.7,10.7,0,0,1-5.11-1.18,8.12,8.12,0,0,1-3.45-3.51,12,12,0,0,1-1.24-5.71A13.57,13.57,0,0,1,640,841a8.32,8.32,0,0,1,7.88-5Zm0,2.73a4.83,4.83,0,0,0-3.77,1.54,7.25,7.25,0,0,0-1.66,4.27h10.37a8,8,0,0,0-.53-3,4.49,4.49,0,0,0-1.61-2A4.92,4.92,0,0,0,647.85,838.71Z"
id="path23" />
<path
class="cls-1"
d="M660.08,843.88h9.19V847h-9.19Z"
id="path24" />
<path
class="cls-1"
d="M686.77,856.71l-8.92-23.77h-.15c0,.51.09,1.15.13,1.94s.07,1.64.1,2.56,0,1.87,0,2.83v16.44h-3.15V829.6h5.05l8.36,22.21h.15l8.5-22.21h5v27.11h-3.38V840q0-1.32,0-2.64t.12-2.46c.05-.78.09-1.43.11-2h-.15l-9,23.73Z"
id="path25" />
<path
class="cls-1"
d="M504.76,822.5l-2.52-2.69a16,16,0,0,1,21.51-.37l-2.42,2.77a12.35,12.35,0,0,0-16.57.29Z"
id="path27" />
<path
class="cls-1"
d="M509.12,826.65l-2.73-2.47a9.18,9.18,0,0,1,13.18-.45L517,826.38a5.51,5.51,0,0,0-7.9.27Z"
id="path28" />
<path
d="M 727.48499,857.1333 H 709.11 v -3.80989 q 1.91406,-1.64063 3.82812,-3.28125 1.93229,-1.64062 3.59115,-3.26302 3.49999,-3.39062 4.79426,-5.3776 1.29427,-2.0052 1.29427,-4.32031 0,-2.11458 -1.40364,-3.29947 -1.38542,-1.20313 -3.88281,-1.20313 -1.65885,0 -3.59114,0.58334 -1.93229,0.58333 -3.77344,1.78645 h -0.18229 v -3.82812 q 1.29427,-0.63802 3.44531,-1.16666 2.16927,-0.52865 4.19271,-0.52865 4.17447,0 6.54426,2.02344 2.36979,2.0052 2.36979,5.45051 0,1.54948 -0.40104,2.89844 -0.38281,1.33073 -1.14844,2.53385 -0.71093,1.13021 -1.67708,2.22396 -0.94791,1.09375 -2.3151,2.42447 -1.95052,1.91406 -4.02864,3.71875 -2.07813,1.78646 -3.88281,3.31771 h 14.60155 z"
id="text1"
style="font-size:37.3333px;fill:#262626"
aria-label="2" />
</g>
<g
id="Layer_2"
data-name="Layer 2">
<rect
class="cls-5"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect28" />
<circle
class="cls-5"
cx="515.99"
cy="794.33"
r="5.48"
id="circle28" />
<circle
class="cls-5"
cx="637.4"
cy="794.33"
r="5.48"
id="circle29" />
<circle
class="cls-5"
cx="660.08"
cy="794.33"
r="5.48"
id="circle30" />
<circle
class="cls-5"
cx="539.09"
cy="794.33"
r="5.48"
id="circle31" />
<circle
class="cls-5"
cx="564.93"
cy="794.33"
r="5.48"
id="circle32" />
<circle
class="cls-5"
cx="590.77"
cy="794.33"
r="5.48"
id="circle33" />
<circle
class="cls-5"
cx="614.09"
cy="794.33"
r="5.48"
id="circle34" />
<path
class="cls-6"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path34" />
<rect
class="cls-7"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect34" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect35" />
<path
class="cls-6"
d="M763.35,462.15v30.34a5.64,5.64,0,0,1-9.62,4L700,442.75a5.63,5.63,0,0,1,4-9.62h30.34A29,29,0,0,1,763.35,462.15Z"
id="path35" />
<rect
class="cls-6"
x="444.77"
y="532.78"
width="290.09"
height="159.13"
rx="18"
id="rect36" />
<rect
class="cls-6"
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect37" />
<path
class="cls-6"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path37" />
<path
class="cls-6"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path38" />
<path
class="cls-6"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path39" />
<path
class="cls-6"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path40" />
<polyline
class="cls-6"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline40" />
<path
class="cls-6"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path41" />
<path
class="cls-6"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path42" />
<path
class="cls-6"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path43" />
<line
class="cls-6"
x1="479.53"
y1="331.65"
x2="479.23"
y2="338.7"
id="line43" />
<line
class="cls-6"
x1="478.57"
y1="354.09"
x2="478.26"
y2="361.13"
id="line44" />
<line
class="cls-6"
x1="574.7"
y1="338.7"
x2="574.41"
y2="331.65"
id="line45" />
<line
class="cls-6"
x1="575.63"
y1="361.13"
x2="575.34"
y2="354.09"
id="line46" />
<polyline
class="cls-6"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect47" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "seeed_xiao_nrf52_kit.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -118,24 +118,6 @@ extension UserDefaults {
@UserDefault(.enableMapPointsOfInterest, defaultValue: false)
static var enableMapPointsOfInterest: Bool
@UserDefault(.enableOfflineMaps, defaultValue: false)
static var enableOfflineMaps: Bool
@UserDefault(.mapTileServer, defaultValue: .openStreetMap)
static var mapTileServer: MapTileServer
@UserDefault(.enableOverlayServer, defaultValue: false)
static var enableOverlayServer: Bool
@UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent)
static var mapOverlayServer: MapOverlayServer
@UserDefault(.mapTilesAboveLabels, defaultValue: false)
static var mapTilesAboveLabels: Bool
@UserDefault(.mapUseLegacy, defaultValue: false)
static var mapUseLegacy: Bool
@UserDefault(.enableDetectionNotifications, defaultValue: false)
static var enableDetectionNotifications: Bool

View file

@ -1,74 +0,0 @@
//
// OfflineTileManager.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 4/23/23.
//
import Foundation
import MapKit
import OSLog
class OfflineTileManager: ObservableObject {
static let shared = OfflineTileManager()
// MARK: - Public properties
@Published var status: DownloadStatus = .downloaded
enum DownloadStatus {
case downloaded, downloading
}
init() {
Logger.services.info("🗂️ Documents Directory = \(self.documentsDirectory.absoluteString, privacy: .public)")
createDirectoriesIfNecessary()
}
// MARK: - Private properties
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) }
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
private let fileManager = FileManager.default
// MARK: - Public methods
func getAllDownloadedSize() -> String {
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
}
func removeAll() {
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
createDirectoriesIfNecessary()
}
func loadAndCacheTileOverlay(for path: MKTileOverlayPath) throws -> Data {
guard UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) else {
return try Data(contentsOf: Bundle.main.url(forResource: "alpha", withExtension: "png")!)
}
let tilesUrl = documentsDirectory
.appendingPathComponent("tiles")
.appendingPathComponent("\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y)")
.appendingPathExtension("png")
do {
return try Data(contentsOf: tilesUrl)
} catch let error as NSError where error.code == NSFileReadNoSuchFileError {
DispatchQueue.main.async { self.status = .downloading }
defer {
DispatchQueue.main.async { self.status = .downloaded }
}
let data = try Data(contentsOf: overlay.url(forTilePath: path))
try data.write(to: tilesUrl)
return data
}
}
// MARK: Private methods
private func createDirectoriesIfNecessary() {
let tiles = documentsDirectory.appendingPathComponent("tiles")
try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:])
}
}

View file

@ -1,15 +0,0 @@
//
// TileOverlay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
import MapKit
class TileOverlay: MKTileOverlay {
override func loadTile(at path: MKTileOverlayPath) async throws -> Data {
return try OfflineTileManager.shared.loadAndCacheTileOverlay(for: path)
}
}

View file

@ -8,6 +8,7 @@
import Foundation
import CocoaMQTT
import OSLog
import Security
protocol MqttClientProxyManagerDelegate: AnyObject {
func onMqttConnected()
@ -40,8 +41,8 @@ class MqttClientProxyManager {
if let host = host {
let port = defaultServerPort
var username = node.mqttConfig?.username
var password = node.mqttConfig?.password
let username = node.mqttConfig?.username
let password = node.mqttConfig?.password
// if host == defaultServerAddress {
//username = ProcessInfo.processInfo.environment["PUBLIC_MQTT_USERNAME"]
//password = ProcessInfo.processInfo.environment["PUBLIC_MQTT_PASSWORD"]
@ -130,6 +131,16 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
self.disconnect()
}
}
func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
let isValid = SecTrustEvaluateWithError(trust, nil)
if isValid {
Logger.mqtt.info("📲 [MQTT Client Proxy] TLS validation succeeded.")
completionHandler(true)
} else {
Logger.mqtt.warning("📲 [MQTT Client Proxy] TLS validation failed.")
completionHandler(true)
}
}
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
Logger.mqtt.debug("📲 [MQTT Client Proxy] disconnected: \(err?.localizedDescription ?? "", privacy: .public)")
if let error = err {

View file

@ -45,6 +45,7 @@ class PersistenceController {
// Merge policy that favors in memory data over data in the db
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
self.container.viewContext.automaticallyMergesChangesFromParent = true
self.container.viewContext.retainsRegisteredObjects = true
if let error = error as NSError? {

View file

@ -543,7 +543,7 @@
"images": [
"t-watch-s3.svg"
],
"partitionScheme": "16MB"
"partitionScheme": "8MB"
},
{
"hwModel": 52,
@ -845,25 +845,32 @@
"hwModelSlug": "THINKNODE_M1",
"platformioTarget": "thinknode_m1",
"architecture": "nrf52840",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 1,
"displayName": "ThinkNode M1",
"tags": [
"Elecrow"
],
"requiresDfu": true
"requiresDfu": true,
"images": [
"thinknode_m1.svg"
],
"hasInkHud": true
},
{
"hwModel": 90,
"hwModelSlug": "THINKNODE_M2",
"platformioTarget": "thinknode_m2",
"architecture": "esp32-s3",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 1,
"displayName": "ThinkNode M2",
"tags": [
"Elecrow"
],
"requiresDfu": false
"requiresDfu": false,
"images": [
"thinknode_m2.svg"
]
}
]

View file

@ -11,6 +11,12 @@ struct ContentView: View {
@ObservedObject
var router: Router
init(appState: AppState, router: Router) {
self.appState = appState
self.router = router
UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified)
}
var body: some View {
TabView(selection: $appState.router.navigationState.selectedTab) {
Messages(

View file

@ -4,29 +4,43 @@ A view draws a circle in the background of the shortName text
*/
import SwiftUI
import CoreData
struct CircleText: View {
var text: String
var color: Color
var text: String
var color: Color
var circleSize: CGFloat = 45
var node: NodeInfoEntity? = nil
var body: some View {
if let node = node {
NavigationStack{
NavigationLink(destination: NodeDetail(node: node)) {
circleContent
}
}
var body: some View {
} else {
circleContent
}
}
ZStack {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
Text(text.addingVariationSelectors)
var circleContent: some View {
ZStack {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
Text(text)
.frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center)
.foregroundColor(color.isLight() ? .black : .white)
.minimumScaleFactor(0.001)
.font(.system(size: 1300))
}
}
}
}
}
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
static var previews: some View {
VStack {
HStack {
CircleText(text: "N1", color: Color.yellow, circleSize: 80)
@ -75,5 +89,5 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
}
}
}
}
}

View file

@ -24,7 +24,7 @@ struct ChannelList: View {
@State private var isPresentingTraceRouteSentAlert = false
var restrictedChannels = ["gpio", "mqtt", "serial"]
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
@ViewBuilder
private func makeChannelRow(

View file

@ -22,128 +22,183 @@ struct ChannelMessageList: View {
@ObservedObject var channel: ChannelEntity
@State private var replyMessageId: Int64 = 0
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
// Scroll state
@State private var showScrollToBottomButton = false
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
var body: some View {
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
if message.replyID > 0 {
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44)
.padding(.all, 5)
.offset(y: -7)
}
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(channel.allPrivateMessages) { (message: MessageEntity) in
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
if message.replyID > 0 {
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
HStack {
MessageText(
message: message,
tapBackDestination: .channel(channel),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if currentUser && message.canRetry {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(
.caption2)
.foregroundColor(.orange)
} else if currentUser && !isDetectionSensorMessage {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
.padding(.bottom)
.id(channel.allPrivateMessages.firstIndex(of: message))
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context))
.padding(.all, 5)
.offset(y: -7)
}
if !currentUser {
Spacer(minLength: 50)
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if !message.read {
message.read = true
do {
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
HStack {
MessageText(
message: message,
tapBackDestination: .channel(channel),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if currentUser && message.canRetry {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(
.caption2)
.foregroundColor(.orange)
} else if currentUser && !isDetectionSensorMessage {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
}
}
.padding(.bottom)
.id(channel.allPrivateMessages.firstIndex(of: message))
if !currentUser {
Spacer(minLength: 50)
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if gotFirstUnreadMessage{
if !message.read {
message.read = true
do {
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
// Check if we've reached the bottom message
if message.messageId == channel.allPrivateMessages.last?.messageId {
hasReachedBottom = true
showScrollToBottomButton = false
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
}
// Invisible spacer to detect reaching bottom
Color.clear
.frame(height: 1)
.id("bottomAnchor")
.onAppear {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
}
}
gotFirstUnreadMessage = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
.onChange(of: channel.allPrivateMessages) {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.onChange(of: channel.allPrivateMessages) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
} else {
showScrollToBottomButton = true
}
}
// Scroll to bottom button
if showScrollToBottomButton {
Button {
withAnimation {
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
} label: {
ScrollToBottomButtonView()
}
.padding(.bottom, 8)
.padding(.trailing, 16)
.transition(.opacity)
}
}
}

View file

@ -20,115 +20,172 @@ struct UserMessageList: View {
// View State Items
@ObservedObject var user: UserEntity
@State private var replyMessageId: Int64 = 0
// Scroll state
@State private var showScrollToBottomButton = false
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
var body: some View {
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach( user.messageList ) { (message: MessageEntity) in
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach( user.messageList ) { (message: MessageEntity) in
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
if message.replyID > 0 {
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
if message.replyID > 0 {
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
HStack(alignment: .top) {
if currentUser { Spacer(minLength: 50) }
VStack(alignment: currentUser ? .trailing : .leading) {
HStack {
MessageText(
message: message,
tapBackDestination: .user(user),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
RetryButton(message: message, destination: .user(user))
}
}
TapbackResponses(message: message) {
appState.unreadDirectMessages = user.unreadMessages
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
// Ack Received
if message.realACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
.font(.caption2)
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
} else {
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
HStack(alignment: .top) {
if currentUser { Spacer(minLength: 50) }
VStack(alignment: currentUser ? .trailing : .leading) {
HStack {
MessageText(
message: message,
tapBackDestination: .user(user),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
RetryButton(message: message, destination: .user(user))
}
}
TapbackResponses(message: message) {
appState.unreadDirectMessages = user.unreadMessages
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
// Ack Received
if message.realACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
.font(.caption2)
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
} else {
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
}
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
} else if currentUser && message.ackError > 0 {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
} else if currentUser && message.ackError > 0 {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
}
}
.padding(.bottom)
.id(user.messageList.firstIndex(of: message))
.padding(.bottom)
.id(user.messageList.firstIndex(of: message))
if !currentUser {
Spacer(minLength: 50)
if !currentUser {
Spacer(minLength: 50)
}
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if !message.read {
message.read = true
do {
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadDirectMessages = user.unreadMessages
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if gotFirstUnreadMessage {
if !message.read {
message.read = true
do {
for unreadMessage in user.messageList.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadDirectMessages = user.unreadMessages
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
// Check if we've reached the bottom message
if message.messageId == user.messageList.last?.messageId {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
}
// Invisible spacer to detect reaching bottom
Color.clear
.frame(height: 1)
.id("bottomAnchor")
.onAppear {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
}
}
gotFirstUnreadMessage = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
.onChange(of: user.messageList) {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.onChange(of: user.messageList) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
} else {
showScrollToBottomButton = true
}
}
// Scroll to bottom button
if showScrollToBottomButton {
Button {
withAnimation {
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
} label: {
ScrollToBottomButtonView()
}
.padding(.bottom, 8)
.padding(.trailing, 16)
.transition(.opacity)
}
}
}

View file

@ -23,10 +23,10 @@ struct PositionPopover: View {
var body: some View {
// Node Color from node.num
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
NavigationStack{
VStack {
HStack {
ZStack {
if position.nodePosition?.isOnline ?? false {
Circle()
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
@ -34,16 +34,15 @@ struct PositionPopover: View {
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale
.repeatForever().delay(delay), value: scale
)
.onAppear {
self.scale = 1
}
.frame(width: 90, height: 90)
}
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65, node: getNodeInfo(id: Int64(position.nodePosition?.user?.num ?? 0), context: context))
}
Text(position.nodePosition?.user?.longName ?? "Unknown")
.font(.largeTitle)
}
@ -106,7 +105,7 @@ struct PositionPopover: View {
.foregroundColor(.primary)
.font(idiom == .phone ? .callout : .body)
}
} icon: {
Image(systemName: "mountain.2.fill")
.symbolRenderingMode(.hierarchical)
@ -147,9 +146,9 @@ struct PositionPopover: View {
Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
} icon: {
Image(systemName: "location.north")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
.padding(.bottom, 5)
/// Distance
@ -181,15 +180,15 @@ struct PositionPopover: View {
}
.padding(.bottom, 5)
if position.nodePosition?.viaMqtt ?? false {
Label {
Text("MQTT")
.font(idiom == .phone ? .callout : .body)
} icon: {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
.padding(.bottom, 5)
}
@ -244,6 +243,7 @@ struct PositionPopover: View {
#endif
}
}
}
.presentationDetents([.fraction(0.65), .large])
.presentationContentInteraction(.scrolls)
.presentationDragIndicator(.visible)

View file

@ -520,6 +520,7 @@ struct NodeDetail: View {
}
}
.listStyle(.insetGrouped)
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
}
}
}

View file

@ -0,0 +1,30 @@
//
// ScrollToBottomButtonView.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 4/2/25.
//
import SwiftUI
struct ScrollToBottomButtonView: View {
var body: some View {
HStack(spacing: 4) {
Text("Jump to present")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.cornerRadius(12)
Image(systemName: "arrow.down")
.font(.title2)
.symbolRenderingMode(.hierarchical)
}
.foregroundColor(.accentColor)
.shadow(radius: 2)
}
}
#Preview {
ScrollToBottomButtonView()
}

View file

@ -264,7 +264,6 @@ struct NodeList: View {
columnVisibility: columnVisibility
)
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(
trailing: ZStack {
if UIDevice.current.userInterfaceIdiom != .phone {

View file

@ -8,7 +8,6 @@ import OSLog
struct AppSettings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@State var totalDownloadedTileSize = ""
@State private var isPresentingCoreDataResetConfirm = false
@State private var isPresentingDeleteMapTilesConfirm = false
@ -85,31 +84,7 @@ struct AppSettings: View {
.foregroundColor(.red)
}
}
if totalDownloadedTileSize != "0MB" {
Section(header: Text("Map Tile Data")) {
Button {
isPresentingDeleteMapTilesConfirm = true
} label: {
Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash")
.foregroundColor(.red)
}
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingDeleteMapTilesConfirm,
titleVisibility: .visible
) {
Button("Delete all map tiles?", role: .destructive) {
tileManager.removeAll()
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
Logger.services.debug("delete all tiles")
}
}
}
}
}
.onAppear(perform: {
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
})
}
.navigationTitle("App Settings")
.navigationBarItems(trailing:

View file

@ -29,9 +29,9 @@ struct BluetoothConfig: View {
Form {
ConfigHeader(title: "Bluetooth", config: \.bluetoothConfig, node: node, onAppear: setBluetoothValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "antenna.radiowaves.left.and.right")
Label("Enabled", systemImage: "antenna.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Pairing Mode", selection: $mode ) {

View file

@ -40,7 +40,7 @@ struct DeviceConfig: View {
Form {
ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
VStack(alignment: .leading) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in

View file

@ -30,7 +30,7 @@ struct AmbientLightingConfig: View {
Form {
ConfigHeader(title: "Ambient Lighting", config: \.ambientLightingConfig, node: node, onAppear: setAmbientLightingConfigValue)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $ledState) {
Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led")

View file

@ -42,11 +42,11 @@ struct CannedMessagesConfig: View {
Form {
ConfigHeader(title: "Canned messages", config: \.cannedMessageConfig, node: node, onAppear: setCannedMessagesValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "list.bullet.rectangle.fill")
Label("Enabled", systemImage: "list.bullet.rectangle.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -47,10 +47,10 @@ struct DetectionSensorConfig: View {
Form {
ConfigHeader(title: "Detection Sensor", config: \.detectionSensorConfig, node: node, onAppear: setDetectionSensorValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.right")
Label("Enabled", systemImage: "dot.radiowaves.right")
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -39,10 +39,10 @@ struct ExternalNotificationConfig: View {
Form {
ConfigHeader(title: "External notification", config: \.externalNotificationConfig, node: node, onAppear: setExternalNotificationValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "megaphone")
Label("Enabled", systemImage: "megaphone")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -30,6 +30,7 @@ struct MQTTConfig: View {
@State var mqttConnected: Bool = false
@State var defaultTopic = "msh/US"
@State var nearbyTopics = [String]()
@State var mapReportingOptIn = false
@State var mapReportingEnabled = false
@State var mapPublishIntervalSecs = 3600
@State var mapPositionPrecision: Double = 14.0
@ -50,10 +51,10 @@ struct MQTTConfig: View {
ConfigHeader(title: "MQTT", config: \.mqttConfig, node: node, onAppear: setMqttValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.up.forward")
Label("Enabled", systemImage: "dot.radiowaves.up.forward")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -66,7 +67,7 @@ struct MQTTConfig: View {
if enabled && proxyToClientEnabled && node?.mqttConfig?.proxyToClientEnabled ?? false == true {
Toggle(isOn: $mqttConnected) {
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
Label("Connect to MQTT via Proxy", systemImage: "server.rack")
if bleManager.mqttError.count > 0 {
Text(bleManager.mqttError)
.fixedSize(horizontal: false, vertical: true)
@ -92,12 +93,30 @@ struct MQTTConfig: View {
}
Section(header: Text("Map Report")) {
Toggle(isOn: $mapReportingEnabled) {
Label("enabled", systemImage: "map")
Label("Enabled", systemImage: "map")
Text("Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if mapReportingEnabled {
Text("Consent to Share Unencrypted Node Data via MQTT")
Text("By enabling this feature, you acknowledge and expressly consent to the transmission of your devices real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions.")
.foregroundColor(.gray)
.font(.caption)
Text("Please be advised that because the map report is not encrypted, your data may be stored and displayed permanently by third parties. Meshtastic does not assume responsibility for any such storage, display or disclosure of this data.")
.foregroundColor(.gray)
.font(.caption)
Toggle(isOn: $mapReportingOptIn) {
Label("I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.", systemImage: "hand.raised")
.foregroundColor(.gray)
.font(.callout)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
if mapReportingEnabled && mapReportingOptIn {
Picker("Map Publish Interval", selection: $mapPublishIntervalSecs ) {
ForEach(UpdateIntervals.allCases) { ui in
if ui.rawValue >= 3600 {
@ -108,6 +127,9 @@ struct MQTTConfig: View {
.pickerStyle(DefaultPickerStyle())
VStack(alignment: .leading) {
Label("Approximate Location", systemImage: "location.slash.circle.fill")
Text("To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy.")
.foregroundColor(.gray)
.font(.callout)
Slider(value: $mapPositionPrecision, in: 11...14, step: 1) {
} minimumValueLabel: {
Image(systemName: "minus")
@ -178,8 +200,8 @@ struct MQTTConfig: View {
.autocorrectionDisabled()
if address != "mqtt.meshtastic.org" {
HStack {
Label("mqtt.username", systemImage: "person.text.rectangle")
TextField("mqtt.username", text: $username)
Label("Username", systemImage: "person.text.rectangle")
TextField("Username", text: $username)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
@ -197,8 +219,8 @@ struct MQTTConfig: View {
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $password)
Label("Password", systemImage: "wallet.pass")
TextField("Password", text: $password)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
@ -244,7 +266,7 @@ struct MQTTConfig: View {
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
mqtt.tlsEnabled = self.tlsEnabled
mqtt.mapReportingEnabled = self.mapReportingEnabled
mqtt.mapReportingEnabled = (self.mapReportingEnabled && self.mapReportingOptIn)
mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision)
mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs)
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
@ -266,6 +288,10 @@ struct MQTTConfig: View {
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}
.onChange(of: address) { _, newAddress in
if address.lowercased() == "mqtt.meshtastic.org" {
username = "meshdev"
password = "large4cats"
}
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
}
.onChange(of: username) { _, newUsername in

View file

@ -26,7 +26,7 @@ struct PaxCounterConfig: View {
Section {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "figure.walk.motion")
Label("Enabled", systemImage: "figure.walk.motion")
Text("config.module.paxcounter.enabled.description")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -46,7 +46,7 @@ struct PaxCounterConfig: View {
.font(.callout)
}
} header: {
Text("options")
Text("Options")
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.powerConfig == nil)

View file

@ -27,9 +27,9 @@ struct RangeTestConfig: View {
Form {
ConfigHeader(title: "Range", config: \.rangeTestConfig, node: node, onAppear: setRangeTestValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "figure.walk")
Label("Enabled", systemImage: "figure.walk")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)

View file

@ -24,7 +24,7 @@ struct RtttlConfig: View {
Form {
ConfigHeader(title: "ringtone", config: \.rtttlConfig, node: node, onAppear: setRtttLConfigValue)
Section(header: Text("options")) {
Section(header: Text("Options")) {
HStack {
Label("ringtone", systemImage: "music.quarternote.3")
TextField("config.ringtone.label", text: $ringtone, axis: .vertical)

View file

@ -33,10 +33,10 @@ struct SerialConfig: View {
Form {
ConfigHeader(title: "Serial", config: \.serialConfig, node: node, onAppear: setSerialValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "terminal")
Label("Enabled", systemImage: "terminal")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -34,9 +34,9 @@ struct StoreForwardConfig: View {
Form {
ConfigHeader(title: "Store & Forward", config: \.storeForwardConfig, node: node, onAppear: setStoreAndForwardValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
Label("Enabled", systemImage: "envelope.arrow.triangle.branch")
Text("Enables the store and forward module.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -64,7 +64,7 @@ struct TelemetryConfig: View {
.foregroundColor(.gray)
.font(.callout)
Toggle(isOn: $environmentMeasurementEnabled) {
Label("enabled", systemImage: "chart.xyaxis.line")
Label("Enabled", systemImage: "chart.xyaxis.line")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $environmentScreenEnabled) {
@ -78,7 +78,7 @@ struct TelemetryConfig: View {
}
Section(header: Text("Power Options")) {
Toggle(isOn: $powerMeasurementEnabled) {
Label("enabled", systemImage: "bolt")
Label("Enabled", systemImage: "bolt")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)

View file

@ -36,7 +36,7 @@ struct NetworkConfig: View {
Section(header: Text("WiFi Options")) {
Toggle(isOn: $wifiEnabled) {
Label("enabled", systemImage: "wifi")
Label("Enabled", systemImage: "wifi")
Text("Enabling WiFi will disable the bluetooth connection to the app.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -82,7 +82,7 @@ struct NetworkConfig: View {
if node.metadata?.hasEthernet ?? false {
Section(header: Text("Ethernet Options")) {
Toggle(isOn: $ethEnabled) {
Label("enabled", systemImage: "network")
Label("Enabled", systemImage: "network")
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -92,7 +92,7 @@ struct NetworkConfig: View {
if node.metadata?.hasEthernet ?? false || node.metadata?.hasWifi ?? false {
Section(header: Text("UDP Broadcast")) {
Toggle(isOn: $udpEnabled) {
Label("enabled", systemImage: "point.3.connected.trianglepath.dotted")
Label("Enabled", systemImage: "point.3.connected.trianglepath.dotted")
Text("Enable broadcasting packets via UDP over the local network.")
}
}

View file

@ -184,7 +184,7 @@ struct Routes: View {
}
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath")
Label("Enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath")
Text("Show on the mesh map.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -31,23 +31,21 @@ struct WidgetsLiveActivity: Widget {
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
if context.state.totalNodes >= 100 {
Text("100+ online")
if context.state.totalNodes > 0 {
Text(" \(context.state.nodesOnline) online")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize()
} else {
Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online")
Text(" ")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize()
}
// Text("\(context.state.channelUtilization.map { String(format: "Ch. Util: %.2f", $0) } ?? "--")%")
Text("Ch. Util: \(context.state.channelUtilization?.formatted(.number.precision(.fractionLength(2))) ?? Constants.nilValueIndicator)%")
.font(.caption2)
.foregroundStyle(.secondary)
.fixedSize()
// Text("\(context.state.airtime.map { String(format: "Airtime: %.2f", $0) } ?? "--")%")
Text("Airtime: \(context.state.airtime?.formatted(.number.precision(.fractionLength(2))) ?? Constants.nilValueIndicator)%")
.font(.caption2)
.foregroundStyle(.secondary)
@ -166,7 +164,7 @@ struct LiveActivityView: View {
.frame(minWidth: 25, idealWidth: 45, maxWidth: 55)
Spacer()
NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets,
dupeReceivedPackets: dupeReceivedPackets, packetsSentRelay: packetsSentRelay, packetsCanceledRelay: packetsCanceledRelay, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange)
dupeReceivedPackets: dupeReceivedPackets, packetsSentRelay: packetsSentRelay, packetsCanceledRelay: packetsCanceledRelay, nodesOnline: nodesOnline, timerRange: timerRange)
Spacer()
}
.tint(.primary)
@ -191,7 +189,6 @@ struct NodeInfoView: View {
var packetsSentRelay: UInt32
var packetsCanceledRelay: UInt32
var nodesOnline: UInt32
var totalNodes: UInt32
var timerRange: ClosedRange<Date>
var body: some View {
@ -220,21 +217,14 @@ struct NodeInfoView: View {
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
if totalNodes >= 100 {
Text("Connected: \(nodesOnline) nodes online")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
} else {
Text("Connected: \(nodesOnline) of \(totalNodes) nodes online")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
}
Text("Connected: \(nodesOnline) nodes online")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
let now = Date()
Text("Last Heard: \(now.formatted())")
.font(.caption)