mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch 'main' into DisconnectSwipe
This commit is contained in:
commit
a1fcb30394
40 changed files with 12540 additions and 4975 deletions
16046
Localizable.xcstrings
16046
Localizable.xcstrings
File diff suppressed because it is too large
Load diff
|
|
@ -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 = "";
|
||||
|
|
|
|||
12
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "thinknode_m1.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
109
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg
vendored
Normal file
109
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg
vendored
Normal 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 |
12
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "thinknode_m2.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
391
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg
vendored
Normal file
391
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg
vendored
Normal 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 |
12
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "seeed_xiao_nrf52_kit.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg
vendored
Normal file
1
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [:])
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -520,6 +520,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
Meshtastic/Views/Nodes/Helpers/ScrollToBottomButton.swift
Normal file
30
Meshtastic/Views/Nodes/Helpers/ScrollToBottomButton.swift
Normal 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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 ) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 device’s 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue