Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-60@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-60@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@1x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@2x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@1x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@2x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@1x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@2x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-76@2x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-83.5@2x-ipad.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-1024-marketing.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
6
mac/src/AppMobile/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
196
mac/src/AppMobile/Help/help.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Help & Quick Start</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--bg: #0e1117;
|
||||||
|
--panel: #131925;
|
||||||
|
--text: #f3f6fb;
|
||||||
|
--muted: #a6b3c7;
|
||||||
|
--accent: #4db2ff;
|
||||||
|
--ok: #4bd37b;
|
||||||
|
--warn: #ff9f43;
|
||||||
|
--line: #2a3346;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--bg: #f6f8fc;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--text: #172033;
|
||||||
|
--muted: #5b6980;
|
||||||
|
--accent: #006fde;
|
||||||
|
--ok: #12834a;
|
||||||
|
--warn: #b85b00;
|
||||||
|
--line: #d7deea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background: radial-gradient(circle at top right, rgba(77, 178, 255, 0.18), transparent 40%), var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 24px 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: color-mix(in oklab, var(--panel) 92%, transparent);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 12px 0 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: color-mix(in oklab, var(--panel) 80%, var(--line));
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Help & Quick Start</h1>
|
||||||
|
<p class="subtitle">A quick guide to reading signals, choosing settings, and troubleshooting.</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Start in 60 Seconds</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Set a symbol like <code>AAPL</code> or <code>BTC-USD</code>.</li>
|
||||||
|
<li>Choose <code>Timeframe</code> (<code>1d</code> is a good default) and <code>Period</code> (<code>6mo</code>).</li>
|
||||||
|
<li>Keep <code>Ignore potentially live last bar</code> enabled.</li>
|
||||||
|
<li>Review trend status and chart markers.</li>
|
||||||
|
<li>Use Export to download CSV/PDF outputs.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Signal Rules</h2>
|
||||||
|
<p>
|
||||||
|
<span class="tag"><span class="ok">real_bull</span> close above previous high</span>
|
||||||
|
<span class="tag"><span class="warn">real_bear</span> close below previous low</span>
|
||||||
|
<span class="tag">fake close inside previous range</span>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Trend starts after <strong>2 consecutive real bars</strong> in the same direction.</li>
|
||||||
|
<li>Trend reverses only after <strong>2 consecutive opposite real bars</strong>.</li>
|
||||||
|
<li>Fake bars are noise and do not reverse trend.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Data Settings</h2>
|
||||||
|
<h3>Core fields</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>Symbol</code>: ticker or pair, e.g. <code>AAPL</code>, <code>MSFT</code>, <code>BTC-USD</code>.</li>
|
||||||
|
<li><code>Timeframe</code>: candle size. Start with <code>1d</code> for cleaner swings.</li>
|
||||||
|
<li><code>Period</code>: amount of history to load. Start with <code>6mo</code>.</li>
|
||||||
|
<li><code>Max bars</code>: limits loaded candles for speed and chart readability.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Optional filters</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>Use previous body range</code>: ignores wick-only breakouts.</li>
|
||||||
|
<li><code>Enable volume filter</code>: treats low-volume bars as fake.</li>
|
||||||
|
<li><code>Hide market-closed gaps</code>: recommended ON for stocks.</li>
|
||||||
|
<li><code>Enable auto-refresh</code>: useful for live monitoring only.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Chart Reading</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Green triangle-up markers show <code>real_bull</code> bars.</li>
|
||||||
|
<li>Red triangle-down markers show <code>real_bear</code> bars.</li>
|
||||||
|
<li>Gray candles (if enabled) de-emphasize fake/noise bars.</li>
|
||||||
|
<li>Volume bars are color-coded by trend state.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Troubleshooting</h2>
|
||||||
|
<ul>
|
||||||
|
<li>If no data appears, verify ticker format (for example <code>BTC-USD</code>, not <code>BTCUSD</code>).</li>
|
||||||
|
<li>If results look noisy, switch to <code>1d</code> and reduce optional filters.</li>
|
||||||
|
<li>If trend seems delayed, remember trend transitions require two real bars.</li>
|
||||||
|
<li>This tool is analysis-only and does not place trades.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
mac/src/AppMobile/ManeshTraderMobileApp.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct ManeshTraderMobileApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
MobileContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
615
mac/src/AppMobile/MobileContentView.swift
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WebKit
|
||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
struct MobileContentView: View {
|
||||||
|
@State private var host = MobileTraderHost()
|
||||||
|
@State private var didAutostart = false
|
||||||
|
@AppStorage("mt_mobile_setup_completed") private var setupCompleted = false
|
||||||
|
@AppStorage("mt_mobile_backend_url") private var storedBackendURL = "http://127.0.0.1:8501"
|
||||||
|
@AppStorage("mt_mobile_symbol") private var storedSymbol = "AAPL"
|
||||||
|
@AppStorage("mt_mobile_interval") private var storedInterval = "1d"
|
||||||
|
@AppStorage("mt_mobile_period") private var storedPeriod = "6mo"
|
||||||
|
@AppStorage("mt_mobile_max_bars") private var storedMaxBars = 500
|
||||||
|
@AppStorage("mt_mobile_drop_live") private var storedDropLive = true
|
||||||
|
@AppStorage("mt_mobile_use_body_range") private var storedUseBodyRange = false
|
||||||
|
@AppStorage("mt_mobile_volume_filter_enabled") private var storedVolumeFilterEnabled = false
|
||||||
|
@AppStorage("mt_mobile_volume_sma_window") private var storedVolumeSMAWindow = 20
|
||||||
|
@AppStorage("mt_mobile_volume_multiplier") private var storedVolumeMultiplier = 1.0
|
||||||
|
@AppStorage("mt_mobile_gray_fake") private var storedGrayFake = true
|
||||||
|
@AppStorage("mt_mobile_hide_market_closed_gaps") private var storedHideMarketClosedGaps = true
|
||||||
|
@AppStorage("mt_mobile_enable_auto_refresh") private var storedEnableAutoRefresh = false
|
||||||
|
@AppStorage("mt_mobile_refresh_sec") private var storedRefreshSeconds = 60
|
||||||
|
|
||||||
|
@State private var showSetupSheet = false
|
||||||
|
@State private var showHelpSheet = false
|
||||||
|
@State private var setupDraft = MobileUserSetupPreferences.default
|
||||||
|
@State private var setupBackendURL = "http://127.0.0.1:8501"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Group {
|
||||||
|
if let url = host.serverURL {
|
||||||
|
MobileWebView(url: url, reloadToken: host.reloadToken)
|
||||||
|
} else {
|
||||||
|
launchView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("ManeshTrader")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||||
|
Button("Help") {
|
||||||
|
showHelpSheet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Setup") {
|
||||||
|
setupDraft = storedWebPreferences.normalized().setupDefaults
|
||||||
|
setupBackendURL = storedBackendURL
|
||||||
|
showSetupSheet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Reload") {
|
||||||
|
_ = syncHostPreferencesFromStorage()
|
||||||
|
host.reloadWebView()
|
||||||
|
}
|
||||||
|
.disabled(host.serverURL == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
guard !didAutostart else { return }
|
||||||
|
didAutostart = true
|
||||||
|
|
||||||
|
let normalized = syncHostPreferencesFromStorage()
|
||||||
|
if !setupCompleted {
|
||||||
|
setupDraft = normalized.setupDefaults
|
||||||
|
setupBackendURL = storedBackendURL
|
||||||
|
showSetupSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showSetupSheet) {
|
||||||
|
setupSheet
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showHelpSheet) {
|
||||||
|
MobileHelpSheetView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storedWebPreferences: MobileWebPreferences {
|
||||||
|
MobileWebPreferences(
|
||||||
|
symbol: storedSymbol,
|
||||||
|
interval: storedInterval,
|
||||||
|
period: storedPeriod,
|
||||||
|
maxBars: storedMaxBars,
|
||||||
|
dropLive: storedDropLive,
|
||||||
|
useBodyRange: storedUseBodyRange,
|
||||||
|
volumeFilterEnabled: storedVolumeFilterEnabled,
|
||||||
|
volumeSMAWindow: storedVolumeSMAWindow,
|
||||||
|
volumeMultiplier: storedVolumeMultiplier,
|
||||||
|
grayFake: storedGrayFake,
|
||||||
|
hideMarketClosedGaps: storedHideMarketClosedGaps,
|
||||||
|
enableAutoRefresh: storedEnableAutoRefresh,
|
||||||
|
refreshSeconds: storedRefreshSeconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var setupSheet: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Text("Connect this app to a reachable ManeshTrader web backend. iOS cannot run the embedded backend executable directly.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Backend") {
|
||||||
|
TextField("Base URL", text: $setupBackendURL)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
.keyboardType(.URL)
|
||||||
|
Text("Examples: http://192.168.1.10:8501 or https://your-host")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Data Defaults") {
|
||||||
|
TextField("Symbol", text: $setupDraft.symbol)
|
||||||
|
.textInputAutocapitalization(.characters)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
|
||||||
|
Picker("Timeframe", selection: $setupDraft.timeframe) {
|
||||||
|
ForEach(MobileUserSetupPreferences.timeframeOptions, id: \.self) { option in
|
||||||
|
Text(option).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker("Period", selection: $setupDraft.period) {
|
||||||
|
ForEach(MobileUserSetupPreferences.periodOptions, id: \.self) { option in
|
||||||
|
Text(option).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stepper(value: $setupDraft.maxBars, in: 20...5000, step: 10) {
|
||||||
|
Text("Max bars: \(setupDraft.maxBars)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let validationMessage = host.validationMessage {
|
||||||
|
Section {
|
||||||
|
Text(validationMessage)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Setup")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Use Defaults") {
|
||||||
|
applySetup(MobileUserSetupPreferences.default, backendURL: setupBackendURL, markCompleted: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Save") {
|
||||||
|
applySetup(setupDraft.normalized(), backendURL: setupBackendURL, markCompleted: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applySetup(_ preferences: MobileUserSetupPreferences, backendURL: String, markCompleted: Bool) {
|
||||||
|
let normalizedSetup = preferences.normalized()
|
||||||
|
let mergedPreferences = storedWebPreferences
|
||||||
|
.applyingSetup(normalizedSetup)
|
||||||
|
.normalized()
|
||||||
|
|
||||||
|
storedBackendURL = backendURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
writeWebPreferencesToStorage(mergedPreferences)
|
||||||
|
|
||||||
|
host.applyPreferences(mergedPreferences, backendURLString: storedBackendURL)
|
||||||
|
host.reloadWebView()
|
||||||
|
|
||||||
|
if markCompleted {
|
||||||
|
setupCompleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if host.serverURL != nil {
|
||||||
|
showSetupSheet = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func syncHostPreferencesFromStorage() -> MobileWebPreferences {
|
||||||
|
let normalized = storedWebPreferences.normalized()
|
||||||
|
writeWebPreferencesToStorage(normalized)
|
||||||
|
host.applyPreferences(normalized, backendURLString: storedBackendURL)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeWebPreferencesToStorage(_ preferences: MobileWebPreferences) {
|
||||||
|
storedSymbol = preferences.symbol
|
||||||
|
storedInterval = preferences.interval
|
||||||
|
storedPeriod = preferences.period
|
||||||
|
storedMaxBars = preferences.maxBars
|
||||||
|
storedDropLive = preferences.dropLive
|
||||||
|
storedUseBodyRange = preferences.useBodyRange
|
||||||
|
storedVolumeFilterEnabled = preferences.volumeFilterEnabled
|
||||||
|
storedVolumeSMAWindow = preferences.volumeSMAWindow
|
||||||
|
storedVolumeMultiplier = preferences.volumeMultiplier
|
||||||
|
storedGrayFake = preferences.grayFake
|
||||||
|
storedHideMarketClosedGaps = preferences.hideMarketClosedGaps
|
||||||
|
storedEnableAutoRefresh = preferences.enableAutoRefresh
|
||||||
|
storedRefreshSeconds = preferences.refreshSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private var launchView: some View {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
Image(systemName: "network.badge.shield.half.filled")
|
||||||
|
.font(.system(size: 42))
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
|
||||||
|
Text("Connect ManeshTrader")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
|
||||||
|
Text("Set a backend URL reachable from this device, then load the web app in place.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: 460)
|
||||||
|
|
||||||
|
Button("Open Setup") {
|
||||||
|
setupDraft = storedWebPreferences.normalized().setupDefaults
|
||||||
|
setupBackendURL = storedBackendURL
|
||||||
|
showSetupSheet = true
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
|
||||||
|
if let validationMessage = host.validationMessage {
|
||||||
|
Text(validationMessage)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: 460)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(uiColor: .systemBackground),
|
||||||
|
Color(uiColor: .secondarySystemBackground),
|
||||||
|
],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MobileHelpSheetView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
private var helpFileURL: URL? {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let candidates = [
|
||||||
|
Bundle.main.url(forResource: "help", withExtension: "html", subdirectory: "Help"),
|
||||||
|
Bundle.main.url(forResource: "help", withExtension: "html"),
|
||||||
|
Bundle.main.resourceURL?.appendingPathComponent("Help/help.html"),
|
||||||
|
Bundle.main.resourceURL?.appendingPathComponent("help.html"),
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
|
return candidates.first(where: { fm.fileExists(atPath: $0.path) })
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Group {
|
||||||
|
if let helpFileURL {
|
||||||
|
MobileHelpDocumentWebView(fileURL: helpFileURL)
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Help file not found.")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
Text("Expected bundled file: help.html")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
.padding(24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Help & Quick Start")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Done") { dismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MobileHelpDocumentWebView: UIViewRepresentable {
|
||||||
|
let fileURL: URL
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
|
let webView = WKWebView(frame: .zero)
|
||||||
|
webView.allowsBackForwardNavigationGestures = true
|
||||||
|
webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent())
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||||
|
if webView.url != fileURL {
|
||||||
|
webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MobileWebView: UIViewRepresentable {
|
||||||
|
let url: URL
|
||||||
|
let reloadToken: Int
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
|
let webView = WKWebView(frame: .zero)
|
||||||
|
webView.customUserAgent = "ManeshTraderMobile"
|
||||||
|
webView.allowsBackForwardNavigationGestures = true
|
||||||
|
webView.navigationDelegate = context.coordinator
|
||||||
|
context.coordinator.attach(webView)
|
||||||
|
webView.load(URLRequest(url: url))
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||||
|
if context.coordinator.lastReloadToken != reloadToken || webView.url != url {
|
||||||
|
context.coordinator.lastReloadToken = reloadToken
|
||||||
|
webView.load(URLRequest(url: url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(url: url, reloadToken: reloadToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, WKNavigationDelegate {
|
||||||
|
private let url: URL
|
||||||
|
private weak var webView: WKWebView?
|
||||||
|
private var pendingRetry: DispatchWorkItem?
|
||||||
|
var lastReloadToken: Int
|
||||||
|
|
||||||
|
init(url: URL, reloadToken: Int) {
|
||||||
|
self.url = url
|
||||||
|
self.lastReloadToken = reloadToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func attach(_ webView: WKWebView) {
|
||||||
|
self.webView = webView
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
|
pendingRetry?.cancel()
|
||||||
|
pendingRetry = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleRetry() {
|
||||||
|
pendingRetry?.cancel()
|
||||||
|
let retry = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self, let webView = self.webView else { return }
|
||||||
|
webView.load(URLRequest(url: self.url))
|
||||||
|
}
|
||||||
|
pendingRetry = retry
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: retry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
@MainActor
|
||||||
|
private final class MobileTraderHost {
|
||||||
|
var reloadToken = 0
|
||||||
|
var validationMessage: String?
|
||||||
|
var webQueryItems: [URLQueryItem] = []
|
||||||
|
var backendURLString = ""
|
||||||
|
|
||||||
|
var serverURL: URL? {
|
||||||
|
guard var components = URLComponents(string: backendURLString),
|
||||||
|
let scheme = components.scheme?.lowercased(),
|
||||||
|
["http", "https"].contains(scheme),
|
||||||
|
components.host != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
components.queryItems = webQueryItems.isEmpty ? nil : webQueryItems
|
||||||
|
return components.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyPreferences(_ preferences: MobileWebPreferences, backendURLString: String) {
|
||||||
|
self.backendURLString = backendURLString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
webQueryItems = preferences.normalized().queryItems
|
||||||
|
|
||||||
|
if serverURL == nil {
|
||||||
|
validationMessage = "Enter a valid backend URL with http:// or https:// and a host."
|
||||||
|
} else {
|
||||||
|
validationMessage = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadWebView() {
|
||||||
|
guard serverURL != nil else { return }
|
||||||
|
reloadToken += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MobileUserSetupPreferences {
|
||||||
|
static let timeframeOptions = ["1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo"]
|
||||||
|
static let periodOptions = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "max"]
|
||||||
|
|
||||||
|
static let `default` = MobileUserSetupPreferences(symbol: "AAPL", timeframe: "1d", period: "6mo", maxBars: 500)
|
||||||
|
|
||||||
|
var symbol: String
|
||||||
|
var timeframe: String
|
||||||
|
var period: String
|
||||||
|
var maxBars: Int
|
||||||
|
|
||||||
|
func normalized() -> MobileUserSetupPreferences {
|
||||||
|
let normalizedSymbol = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||||
|
let safeSymbol = normalizedSymbol.isEmpty ? "AAPL" : normalizedSymbol
|
||||||
|
|
||||||
|
let safeTimeframe = Self.timeframeOptions.contains(timeframe) ? timeframe : "1d"
|
||||||
|
let safePeriod = Self.periodOptions.contains(period) ? period : "6mo"
|
||||||
|
let safeMaxBars = min(5000, max(20, maxBars))
|
||||||
|
|
||||||
|
return MobileUserSetupPreferences(
|
||||||
|
symbol: safeSymbol,
|
||||||
|
timeframe: safeTimeframe,
|
||||||
|
period: safePeriod,
|
||||||
|
maxBars: safeMaxBars
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MobileWebPreferences: Codable {
|
||||||
|
var symbol: String
|
||||||
|
var interval: String
|
||||||
|
var period: String
|
||||||
|
var maxBars: Int
|
||||||
|
var dropLive: Bool
|
||||||
|
var useBodyRange: Bool
|
||||||
|
var volumeFilterEnabled: Bool
|
||||||
|
var volumeSMAWindow: Int
|
||||||
|
var volumeMultiplier: Double
|
||||||
|
var grayFake: Bool
|
||||||
|
var hideMarketClosedGaps: Bool
|
||||||
|
var enableAutoRefresh: Bool
|
||||||
|
var refreshSeconds: Int
|
||||||
|
|
||||||
|
init(
|
||||||
|
symbol: String,
|
||||||
|
interval: String,
|
||||||
|
period: String,
|
||||||
|
maxBars: Int,
|
||||||
|
dropLive: Bool,
|
||||||
|
useBodyRange: Bool,
|
||||||
|
volumeFilterEnabled: Bool,
|
||||||
|
volumeSMAWindow: Int,
|
||||||
|
volumeMultiplier: Double,
|
||||||
|
grayFake: Bool,
|
||||||
|
hideMarketClosedGaps: Bool,
|
||||||
|
enableAutoRefresh: Bool,
|
||||||
|
refreshSeconds: Int
|
||||||
|
) {
|
||||||
|
self.symbol = symbol
|
||||||
|
self.interval = interval
|
||||||
|
self.period = period
|
||||||
|
self.maxBars = maxBars
|
||||||
|
self.dropLive = dropLive
|
||||||
|
self.useBodyRange = useBodyRange
|
||||||
|
self.volumeFilterEnabled = volumeFilterEnabled
|
||||||
|
self.volumeSMAWindow = volumeSMAWindow
|
||||||
|
self.volumeMultiplier = volumeMultiplier
|
||||||
|
self.grayFake = grayFake
|
||||||
|
self.hideMarketClosedGaps = hideMarketClosedGaps
|
||||||
|
self.enableAutoRefresh = enableAutoRefresh
|
||||||
|
self.refreshSeconds = refreshSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
static let `default` = MobileWebPreferences(
|
||||||
|
symbol: "AAPL",
|
||||||
|
interval: "1d",
|
||||||
|
period: "6mo",
|
||||||
|
maxBars: 500,
|
||||||
|
dropLive: true,
|
||||||
|
useBodyRange: false,
|
||||||
|
volumeFilterEnabled: false,
|
||||||
|
volumeSMAWindow: 20,
|
||||||
|
volumeMultiplier: 1.0,
|
||||||
|
grayFake: true,
|
||||||
|
hideMarketClosedGaps: true,
|
||||||
|
enableAutoRefresh: false,
|
||||||
|
refreshSeconds: 60
|
||||||
|
)
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case symbol
|
||||||
|
case interval
|
||||||
|
case period
|
||||||
|
case maxBars = "max_bars"
|
||||||
|
case dropLive = "drop_live"
|
||||||
|
case useBodyRange = "use_body_range"
|
||||||
|
case volumeFilterEnabled = "volume_filter_enabled"
|
||||||
|
case volumeSMAWindow = "volume_sma_window"
|
||||||
|
case volumeMultiplier = "volume_multiplier"
|
||||||
|
case grayFake = "gray_fake"
|
||||||
|
case hideMarketClosedGaps = "hide_market_closed_gaps"
|
||||||
|
case enableAutoRefresh = "enable_auto_refresh"
|
||||||
|
case refreshSeconds = "refresh_sec"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let defaults = Self.default
|
||||||
|
|
||||||
|
symbol = try container.decodeIfPresent(String.self, forKey: .symbol) ?? defaults.symbol
|
||||||
|
interval = try container.decodeIfPresent(String.self, forKey: .interval) ?? defaults.interval
|
||||||
|
period = try container.decodeIfPresent(String.self, forKey: .period) ?? defaults.period
|
||||||
|
maxBars = try container.decodeIfPresent(Int.self, forKey: .maxBars) ?? defaults.maxBars
|
||||||
|
dropLive = try container.decodeIfPresent(Bool.self, forKey: .dropLive) ?? defaults.dropLive
|
||||||
|
useBodyRange = try container.decodeIfPresent(Bool.self, forKey: .useBodyRange) ?? defaults.useBodyRange
|
||||||
|
volumeFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: .volumeFilterEnabled) ?? defaults.volumeFilterEnabled
|
||||||
|
volumeSMAWindow = try container.decodeIfPresent(Int.self, forKey: .volumeSMAWindow) ?? defaults.volumeSMAWindow
|
||||||
|
volumeMultiplier = try container.decodeIfPresent(Double.self, forKey: .volumeMultiplier) ?? defaults.volumeMultiplier
|
||||||
|
grayFake = try container.decodeIfPresent(Bool.self, forKey: .grayFake) ?? defaults.grayFake
|
||||||
|
hideMarketClosedGaps = try container.decodeIfPresent(Bool.self, forKey: .hideMarketClosedGaps) ?? defaults.hideMarketClosedGaps
|
||||||
|
enableAutoRefresh = try container.decodeIfPresent(Bool.self, forKey: .enableAutoRefresh) ?? defaults.enableAutoRefresh
|
||||||
|
refreshSeconds = try container.decodeIfPresent(Int.self, forKey: .refreshSeconds) ?? defaults.refreshSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalized() -> MobileWebPreferences {
|
||||||
|
let safeSymbol = {
|
||||||
|
let candidate = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||||
|
return candidate.isEmpty ? "AAPL" : candidate
|
||||||
|
}()
|
||||||
|
|
||||||
|
let safeInterval = MobileUserSetupPreferences.timeframeOptions.contains(interval) ? interval : "1d"
|
||||||
|
let safePeriod = MobileUserSetupPreferences.periodOptions.contains(period) ? period : "6mo"
|
||||||
|
let safeMaxBars = min(5000, max(20, maxBars))
|
||||||
|
let safeVolumeSMAWindow = min(100, max(2, volumeSMAWindow))
|
||||||
|
let clampedMultiplier = min(3.0, max(0.1, volumeMultiplier))
|
||||||
|
let safeVolumeMultiplier = (clampedMultiplier * 10).rounded() / 10
|
||||||
|
let safeRefreshSeconds = min(600, max(10, refreshSeconds))
|
||||||
|
|
||||||
|
return MobileWebPreferences(
|
||||||
|
symbol: safeSymbol,
|
||||||
|
interval: safeInterval,
|
||||||
|
period: safePeriod,
|
||||||
|
maxBars: safeMaxBars,
|
||||||
|
dropLive: dropLive,
|
||||||
|
useBodyRange: useBodyRange,
|
||||||
|
volumeFilterEnabled: volumeFilterEnabled,
|
||||||
|
volumeSMAWindow: safeVolumeSMAWindow,
|
||||||
|
volumeMultiplier: safeVolumeMultiplier,
|
||||||
|
grayFake: grayFake,
|
||||||
|
hideMarketClosedGaps: hideMarketClosedGaps,
|
||||||
|
enableAutoRefresh: enableAutoRefresh,
|
||||||
|
refreshSeconds: safeRefreshSeconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupDefaults: MobileUserSetupPreferences {
|
||||||
|
MobileUserSetupPreferences(
|
||||||
|
symbol: symbol,
|
||||||
|
timeframe: interval,
|
||||||
|
period: period,
|
||||||
|
maxBars: maxBars
|
||||||
|
).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyingSetup(_ setup: MobileUserSetupPreferences) -> MobileWebPreferences {
|
||||||
|
let normalizedSetup = setup.normalized()
|
||||||
|
return MobileWebPreferences(
|
||||||
|
symbol: normalizedSetup.symbol,
|
||||||
|
interval: normalizedSetup.timeframe,
|
||||||
|
period: normalizedSetup.period,
|
||||||
|
maxBars: normalizedSetup.maxBars,
|
||||||
|
dropLive: dropLive,
|
||||||
|
useBodyRange: useBodyRange,
|
||||||
|
volumeFilterEnabled: volumeFilterEnabled,
|
||||||
|
volumeSMAWindow: volumeSMAWindow,
|
||||||
|
volumeMultiplier: volumeMultiplier,
|
||||||
|
grayFake: grayFake,
|
||||||
|
hideMarketClosedGaps: hideMarketClosedGaps,
|
||||||
|
enableAutoRefresh: enableAutoRefresh,
|
||||||
|
refreshSeconds: refreshSeconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem] {
|
||||||
|
[
|
||||||
|
URLQueryItem(name: "symbol", value: symbol),
|
||||||
|
URLQueryItem(name: "interval", value: interval),
|
||||||
|
URLQueryItem(name: "period", value: period),
|
||||||
|
URLQueryItem(name: "max_bars", value: String(maxBars)),
|
||||||
|
URLQueryItem(name: "drop_live", value: String(dropLive)),
|
||||||
|
URLQueryItem(name: "use_body_range", value: String(useBodyRange)),
|
||||||
|
URLQueryItem(name: "volume_filter_enabled", value: String(volumeFilterEnabled)),
|
||||||
|
URLQueryItem(name: "volume_sma_window", value: String(volumeSMAWindow)),
|
||||||
|
URLQueryItem(name: "volume_multiplier", value: String(volumeMultiplier)),
|
||||||
|
URLQueryItem(name: "gray_fake", value: String(grayFake)),
|
||||||
|
URLQueryItem(name: "hide_market_closed_gaps", value: String(hideMarketClosedGaps)),
|
||||||
|
URLQueryItem(name: "enable_auto_refresh", value: String(enableAutoRefresh)),
|
||||||
|
URLQueryItem(name: "refresh_sec", value: String(refreshSeconds)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
MobileContentView()
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMac.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMac.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@ -35,6 +36,11 @@
|
|||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EAB661012F3FD5C100ED41BA /* AppMobile */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = AppMobile;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
|
EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = AppTests;
|
path = AppTests;
|
||||||
@ -55,6 +61,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EAB661032F3FD5C100ED41BA /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
EAB660762F3FD5C100ED41BA /* Frameworks */ = {
|
EAB660762F3FD5C100ED41BA /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -76,6 +89,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EAB6606E2F3FD5C000ED41BA /* App */,
|
EAB6606E2F3FD5C000ED41BA /* App */,
|
||||||
|
EAB661012F3FD5C100ED41BA /* AppMobile */,
|
||||||
EAB6607C2F3FD5C100ED41BA /* AppTests */,
|
EAB6607C2F3FD5C100ED41BA /* AppTests */,
|
||||||
EAB660862F3FD5C100ED41BA /* AppUITests */,
|
EAB660862F3FD5C100ED41BA /* AppUITests */,
|
||||||
EAB6606D2F3FD5C000ED41BA /* Products */,
|
EAB6606D2F3FD5C000ED41BA /* Products */,
|
||||||
@ -86,6 +100,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
|
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
|
||||||
|
EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */,
|
||||||
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
|
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
|
||||||
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
|
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
|
||||||
);
|
);
|
||||||
@ -117,6 +132,28 @@
|
|||||||
productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */;
|
productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = EAB661082F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMobile" */;
|
||||||
|
buildPhases = (
|
||||||
|
EAB661052F3FD5C100ED41BA /* Sources */,
|
||||||
|
EAB661032F3FD5C100ED41BA /* Frameworks */,
|
||||||
|
EAB661042F3FD5C100ED41BA /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
EAB661012F3FD5C100ED41BA /* AppMobile */,
|
||||||
|
);
|
||||||
|
name = ManeshTraderMobile;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = ManeshTraderMobile;
|
||||||
|
productReference = EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
|
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */;
|
buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */;
|
||||||
@ -176,6 +213,9 @@
|
|||||||
EAB6606B2F3FD5C000ED41BA = {
|
EAB6606B2F3FD5C000ED41BA = {
|
||||||
CreatedOnToolsVersion = 26.3;
|
CreatedOnToolsVersion = 26.3;
|
||||||
};
|
};
|
||||||
|
EAB661022F3FD5C100ED41BA = {
|
||||||
|
CreatedOnToolsVersion = 26.3;
|
||||||
|
};
|
||||||
EAB660782F3FD5C100ED41BA = {
|
EAB660782F3FD5C100ED41BA = {
|
||||||
CreatedOnToolsVersion = 26.3;
|
CreatedOnToolsVersion = 26.3;
|
||||||
TestTargetID = EAB6606B2F3FD5C000ED41BA;
|
TestTargetID = EAB6606B2F3FD5C000ED41BA;
|
||||||
@ -201,6 +241,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */,
|
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */,
|
||||||
|
EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */,
|
||||||
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */,
|
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */,
|
||||||
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */,
|
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */,
|
||||||
);
|
);
|
||||||
@ -215,6 +256,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EAB661042F3FD5C100ED41BA /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
EAB660772F3FD5C100ED41BA /* Resources */ = {
|
EAB660772F3FD5C100ED41BA /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -239,6 +287,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EAB661052F3FD5C100ED41BA /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
EAB660752F3FD5C100ED41BA /* Sources */ = {
|
EAB660752F3FD5C100ED41BA /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -457,6 +512,64 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
EAB661062F3FD5C100ED41BA /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMobile;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
EAB661072F3FD5C100ED41BA /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMobile;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
EAB660912F3FD5C100ED41BA /* Debug */ = {
|
EAB660912F3FD5C100ED41BA /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@ -558,6 +671,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
EAB661082F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMobile" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
EAB661062F3FD5C100ED41BA /* Debug */,
|
||||||
|
EAB661072F3FD5C100ED41BA /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */ = {
|
EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2630"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
|
||||||
|
BuildableName = "ManeshTraderMobile.app"
|
||||||
|
BlueprintName = "ManeshTraderMobile"
|
||||||
|
ReferencedContainer = "container:MacShell.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
|
||||||
|
BuildableName = "ManeshTraderMobile.app"
|
||||||
|
BlueprintName = "ManeshTraderMobile"
|
||||||
|
ReferencedContainer = "container:MacShell.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
|
||||||
|
BuildableName = "ManeshTraderMobile.app"
|
||||||
|
BlueprintName = "ManeshTraderMobile"
|
||||||
|
ReferencedContainer = "container:MacShell.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@ -27,6 +27,12 @@ From repo root:
|
|||||||
3. Build/Run scheme:
|
3. Build/Run scheme:
|
||||||
- default: project name (or set `MAC_SCHEME` in scripts)
|
- default: project name (or set `MAC_SCHEME` in scripts)
|
||||||
|
|
||||||
|
## iOS/iPadOS Target
|
||||||
|
- New target: `ManeshTraderMobile` (same Xcode project: `mac/src/MacShell.xcodeproj`).
|
||||||
|
- The mobile target is a web wrapper and does **not** launch the embedded backend executable.
|
||||||
|
- In the app, open `Setup` and provide a backend URL reachable from the device/simulator (for example `http://<LAN-IP>:8501` or an HTTPS host).
|
||||||
|
- The same symbol/timeframe/period preferences are passed as query params to the backend URL.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`).
|
- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`).
|
||||||
- Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory.
|
- Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory.
|
||||||
|
|||||||