mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 02:14:43 +03:00
IOS codes
This commit is contained in:
@@ -30,3 +30,11 @@ todo.md
|
||||
|
||||
tmp
|
||||
.claude
|
||||
|
||||
# iOS build artifacts
|
||||
ios/Mobile.xcframework
|
||||
ios/build
|
||||
ios/DerivedData
|
||||
ios/*.xcuserdata
|
||||
ios/Thefeed.xcodeproj/xcuserdata
|
||||
ios/Thefeed.xcodeproj/project.xcworkspace/xcuserdata
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.PHONY: all build build-server build-client test clean lint fmt vet
|
||||
.PHONY: all build build-server build-client test clean lint fmt vet \
|
||||
ios-bind ios-bind-catalyst ios-build ios-test ios-clean ios-list-sims ios-deps
|
||||
|
||||
BINARY_SERVER = thefeed-server
|
||||
BINARY_CLIENT = thefeed-client
|
||||
@@ -97,6 +98,46 @@ build-android-arm:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build $(call CLIENT_GOFLAGS,thefeed-client-android-arm) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm ./cmd/client
|
||||
|
||||
# ===== iOS / Mac Catalyst =====
|
||||
# Requires: Xcode + gomobile (go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init)
|
||||
|
||||
IOS_DIR = ios
|
||||
IOS_FRAMEWORK = $(IOS_DIR)/Mobile.xcframework
|
||||
IOS_SCHEME = Thefeed
|
||||
IOS_PROJECT = $(IOS_DIR)/Thefeed.xcodeproj
|
||||
# Default simulator: pick the first available iPhone (override with IOS_SIM_NAME='iPhone 17').
|
||||
IOS_SIM_NAME ?= $(shell xcrun simctl list devices available 2>/dev/null | awk -F'[()]' '/-- iOS [0-9]/{ios=1;next} /^-- /{ios=0} ios && /iPhone/{print $$1; exit}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$$//')
|
||||
|
||||
ios-deps:
|
||||
@grep -q "golang.org/x/mobile" go.mod || go get golang.org/x/mobile/bind golang.org/x/mobile/bind/objc
|
||||
go mod tidy
|
||||
|
||||
ios-bind: ios-deps
|
||||
@command -v gomobile >/dev/null 2>&1 || { echo "gomobile not found. Run: go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init"; exit 1; }
|
||||
gomobile bind -iosversion=14.0 -target=ios,iossimulator -o $(IOS_FRAMEWORK) ./mobile
|
||||
|
||||
ios-bind-catalyst: ios-deps
|
||||
@command -v gomobile >/dev/null 2>&1 || { echo "gomobile not found"; exit 1; }
|
||||
gomobile bind -iosversion=14.0 -target=ios,iossimulator,maccatalyst -o $(IOS_FRAMEWORK) ./mobile
|
||||
|
||||
ios-list-sims:
|
||||
xcrun simctl list devices available
|
||||
|
||||
ios-build: $(IOS_FRAMEWORK)
|
||||
xcodebuild -project $(IOS_PROJECT) -scheme $(IOS_SCHEME) \
|
||||
-destination 'platform=iOS Simulator,name=$(IOS_SIM_NAME)' \
|
||||
build
|
||||
|
||||
ios-test: $(IOS_FRAMEWORK)
|
||||
xcodebuild test -project $(IOS_PROJECT) -scheme $(IOS_SCHEME) \
|
||||
-destination 'platform=iOS Simulator,name=$(IOS_SIM_NAME)'
|
||||
|
||||
$(IOS_FRAMEWORK):
|
||||
$(MAKE) ios-bind
|
||||
|
||||
ios-clean:
|
||||
rm -rf $(IOS_FRAMEWORK) $(IOS_DIR)/build $(IOS_DIR)/DerivedData
|
||||
|
||||
# UPX compression (requires upx in PATH) — only for Linux/Windows binaries
|
||||
upx:
|
||||
@command -v upx >/dev/null 2>&1 || { echo "upx not found, skipping compression"; exit 0; }
|
||||
|
||||
@@ -362,6 +362,19 @@ chmod +x thefeed-client
|
||||
|
||||
برنامه در اولین اجرا اجازهی Battery Optimization Exemption را میگیرد تا سرویس پسزمینه توسط سیستم بسته نشود.
|
||||
|
||||
### iOS
|
||||
|
||||
نسخهی iOS در حال توسعه است. سورس در پوشهی [`ios/`](ios/) قرار دارد. برای ساخت روی مک:
|
||||
|
||||
```
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init
|
||||
make ios-bind # ساخت Mobile.xcframework
|
||||
make ios-build # بیلد روی iOS Simulator
|
||||
make ios-test # اجرای تستها
|
||||
```
|
||||
|
||||
سپس `ios/Thefeed.xcodeproj` را در Xcode باز کنید.
|
||||
|
||||
## ⚙️ تنظیمات DNS
|
||||
|
||||
شما به **دو رکورد DNS** نیاز دارید. فرض کنید IP سرور شما `203.0.113.10` است:
|
||||
|
||||
@@ -12,6 +12,7 @@ DNS-based feed reader for Telegram channels and public X accounts. Designed for
|
||||
sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh)"
|
||||
```
|
||||
- **Android APK** (Android 7.0+): pick `arm64-v8a` for any phone newer than ~2017, `armeabi-v7a` for older 32-bit-only devices.
|
||||
- **iOS** (iOS 14+): App Store build planned. Source under [ios/](ios/) — see [iOS development](#ios-development) below.
|
||||
|
||||
Public configs to test with: [@thefeedconfig](https://t.me/thefeedconfig).
|
||||
|
||||
@@ -529,6 +530,31 @@ make fmt # Format code
|
||||
make clean # Remove build artifacts
|
||||
```
|
||||
|
||||
## iOS development
|
||||
|
||||
Wraps the Go client as a gomobile-bound xcframework consumed by a SwiftUI app under `ios/`. Server runs in-process on `127.0.0.1:<random-port>`; foreground only (iOS does not allow long-lived background servers).
|
||||
|
||||
Prereqs on macOS: Xcode 15+, Go 1.22+, gomobile.
|
||||
|
||||
```
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
```
|
||||
|
||||
Common targets:
|
||||
|
||||
```
|
||||
make ios-bind # build Mobile.xcframework (iOS device + Simulator)
|
||||
make ios-bind-catalyst # also include Mac Catalyst slice
|
||||
make ios-build # build the app for the Simulator
|
||||
make ios-test # run unit tests on the Simulator
|
||||
make ios-list-sims # list available simulator destinations
|
||||
```
|
||||
|
||||
Override the default simulator with `IOS_SIM_NAME='iPhone 16'`.
|
||||
|
||||
Open `ios/Thefeed.xcodeproj` in Xcode after `make ios-bind` to run from Xcode.
|
||||
|
||||
## Releases (GitHub Actions)
|
||||
|
||||
Pushing a tag that starts with `v` triggers CI build + GitHub Release.
|
||||
|
||||
@@ -6,9 +6,10 @@ require (
|
||||
github.com/gotd/td v0.142.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/refraction-networking/utls v1.6.7
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/term v0.42.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -40,11 +41,11 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
@@ -81,26 +81,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -265,13 +265,18 @@ func TestPersistScanResultsRescanOverwrites(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistScanResultsNoSelectedList(t *testing.T) {
|
||||
func TestPersistScanResultsSeedsDefaultListOnFirstRun(t *testing.T) {
|
||||
s := newTestServerWithProfiles(t, &ProfileList{})
|
||||
// No-op when there's no selected list to write into.
|
||||
s.persistScanResultsToList([]string{"a:53"})
|
||||
s.persistScanResultsToList([]string{"a:53", "b:53"})
|
||||
pl := loadProfilesT(t, s)
|
||||
if len(pl.ActiveLists) != 0 {
|
||||
t.Errorf("ActiveLists got created: %v", pl.ActiveLists)
|
||||
if len(pl.ActiveLists) != 1 || pl.ActiveLists[0].Name != defaultListName {
|
||||
t.Fatalf("ActiveLists = %v, want one Default list", pl.ActiveLists)
|
||||
}
|
||||
if got := pl.ActiveLists[0].Resolvers; len(got) != 2 {
|
||||
t.Errorf("Default list = %v, want 2 entries", got)
|
||||
}
|
||||
if pl.SelectedList != defaultListName {
|
||||
t.Errorf("SelectedList = %q, want %q", pl.SelectedList, defaultListName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,12 +202,12 @@
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding: 7px 12px;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
width: 100%
|
||||
}
|
||||
@@ -959,7 +959,11 @@
|
||||
background: var(--bg);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
color: var(--text)
|
||||
color: var(--text);
|
||||
padding-top: var(--safe-top);
|
||||
padding-bottom: var(--safe-bottom);
|
||||
padding-left: var(--safe-left);
|
||||
padding-right: var(--safe-right)
|
||||
}
|
||||
.tm-modal.active { display: flex }
|
||||
|
||||
@@ -3271,9 +3275,9 @@
|
||||
<div class="modal">
|
||||
<h2 id="profileEditorTitle" data-i18n="new_profile">New Profile</h2>
|
||||
<div id="peWarning" class="tg-warning"></div>
|
||||
<div class="form-group"><label data-i18n="nickname">Nickname</label><input id="peNick" maxlength="32" placeholder="My Server">
|
||||
<div class="form-group"><label data-i18n="nickname">Nickname</label><input id="peNick" maxlength="32" placeholder="My Server" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group"><label data-i18n="domain">Domain</label><input id="peDomain" placeholder="t.example.com">
|
||||
<div class="form-group"><label data-i18n="domain">Domain</label><input id="peDomain" placeholder="t.example.com" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group"><label data-i18n="passphrase">Passphrase</label><input type="password" id="peKey"
|
||||
placeholder="..."></div>
|
||||
@@ -4030,7 +4034,15 @@
|
||||
poll_placeholder: 'Poll (open Telegram to view)',
|
||||
}
|
||||
};
|
||||
var lang = localStorage.getItem('thefeed_lang') || 'fa';
|
||||
// Order: native bridge (iOS/Android) → localStorage → 'fa'.
|
||||
// iOS localStorage is per-origin (host:port) so picking up the
|
||||
// native value first survives an embedded server rebinding to a
|
||||
// new loopback port on each launch.
|
||||
var lang = (function () {
|
||||
try { if (typeof IOS !== 'undefined' && IOS.getLang) { var v = IOS.getLang(); if (v) return v; } } catch (e) { }
|
||||
try { if (typeof Android !== 'undefined' && Android.getLang) { var v2 = Android.getLang(); if (v2) return v2; } } catch (e) { }
|
||||
return localStorage.getItem('thefeed_lang') || 'fa';
|
||||
})();
|
||||
function t(k) { return (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k }
|
||||
function applyLang() {
|
||||
var isRtl = lang === 'fa';
|
||||
@@ -4051,6 +4063,7 @@
|
||||
localStorage.setItem('thefeed_lang', l);
|
||||
applyLang();
|
||||
if (typeof Android !== 'undefined') try { Android.setLang(l) } catch (e) { }
|
||||
if (typeof IOS !== 'undefined' && IOS.setLang) try { IOS.setLang(l) } catch (e) { }
|
||||
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang: l }) }).catch(function () { });
|
||||
}
|
||||
|
||||
@@ -4126,7 +4139,10 @@
|
||||
});
|
||||
function filterChannels() {
|
||||
var q = document.getElementById('channelSearch').value.toLowerCase();
|
||||
document.querySelectorAll('.ch-item').forEach(function (el) { el.style.display = el.dataset.name.toLowerCase().includes(q) ? 'flex' : 'none' });
|
||||
document.querySelectorAll('.ch-item').forEach(function (el) {
|
||||
var hay = (el.dataset.name + ' ' + (el.dataset.label || '')).toLowerCase();
|
||||
el.style.display = hay.includes(q) ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// ===== INIT =====
|
||||
@@ -5552,7 +5568,7 @@
|
||||
// No 8.8.8.8 / 1.1.1.1 fallback — an empty resolver list in
|
||||
// the URI is the sender's deliberate choice (e.g. they want
|
||||
// the recipient to bring their own bank).
|
||||
sharedNick = sharedNick.replace(/[\x00-\x1f\x7f]/g, '').trim().slice(0, 32);
|
||||
sharedNick = sanitizeNickname(sharedNick);
|
||||
// Only ask about bank merging when the URI actually carried
|
||||
// resolvers — empty list means the sender shared zero, so
|
||||
// there's nothing to merge and no prompt to show.
|
||||
@@ -5664,12 +5680,31 @@
|
||||
} catch (e) { showToast(e.message) }
|
||||
}
|
||||
|
||||
// sanitizeNickname extracts a clean alias from raw input. If the user
|
||||
// pasted a thefeed:// URI, pulls the n= param. Strips control chars,
|
||||
// the clipboard emoji, newlines, and trims to 32 chars.
|
||||
function sanitizeNickname(raw) {
|
||||
var v = String(raw || '');
|
||||
var m = v.match(/thefeed:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:[?&]n=([^&\s#]*))/i);
|
||||
if (m && m[1]) {
|
||||
try { v = decodeURIComponent(m[1].replace(/\+/g, ' ')); } catch (e) { v = m[1]; }
|
||||
}
|
||||
// iOS Quick Type appends UI text starting with the clipboard glyph
|
||||
// (U+1F4CB) when the user manually pastes — cut from there.
|
||||
var clipIdx = v.indexOf('📋');
|
||||
if (clipIdx >= 0) v = v.slice(0, clipIdx);
|
||||
// Strip control / zero-width / bidi / BOM characters.
|
||||
v = v.replace(/[\u0000-\u001F\u007F\u200B-\u200F\u2028\u2029\uFEFF]/g, "");
|
||||
v = v.replace(/\s+/g, ' ').trim();
|
||||
return v.slice(0, 32);
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
var errEl = document.getElementById('peError'); errEl.style.display = 'none';
|
||||
// Trim and clamp the nickname server-side too: the maxlength
|
||||
// attribute on the input only stops keyboard typing, not paste
|
||||
// / programmatic value setting.
|
||||
var nick = document.getElementById('peNick').value.trim().slice(0, 32);
|
||||
var nick = sanitizeNickname(document.getElementById('peNick').value);
|
||||
var domain = document.getElementById('peDomain').value.trim();
|
||||
var key = document.getElementById('peKey').value;
|
||||
if (!domain || !key) { errEl.textContent = t('domain') + ' / ' + t('passphrase'); errEl.style.display = 'block'; return }
|
||||
@@ -7757,7 +7792,7 @@
|
||||
document.getElementById('progressPanel').innerHTML = '';
|
||||
var p = document.getElementById('progressPanel');
|
||||
p.innerHTML = '<div class="progress-item" id="prog-init" data-last-update="' + Date.now() + '"><button class="progress-close" onclick="this.parentNode.remove()" title="Dismiss">×</button><div class="progress-label">' + t('loading') + '</div><div class="progress-bar"><div class="progress-fill" style="width:30%;animation:prog-pulse 1.5s ease-in-out infinite"></div></div></div>';
|
||||
if (mobileQuery.matches) { openChat(); openLog(); }
|
||||
if (mobileQuery.matches) { openChat(); }
|
||||
}
|
||||
function startAutoRefresh() { if (autoRefreshTimer) return; autoRefreshTimer = setInterval(function () { if (selectedChannel > 0) doRefresh(true) }, 600000) }
|
||||
function updateNextFetchDisplay() {
|
||||
@@ -8734,4 +8769,4 @@
|
||||
<!-- END telemirror -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+18
-3
@@ -13,6 +13,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
mrand "math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -301,8 +302,15 @@ func New(dataDir string, port int, host string, password string) (*Server, error
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Run starts the web server.
|
||||
func (s *Server) Run() error {
|
||||
// Run starts the web server, binding to s.host:s.port.
|
||||
func (s *Server) Run() error { return s.serve(nil) }
|
||||
|
||||
// Serve runs the web server on an already-bound listener. Used by the
|
||||
// mobile entry where the listener is opened first to discover the
|
||||
// kernel-assigned port.
|
||||
func (s *Server) Serve(ln net.Listener) error { return s.serve(ln) }
|
||||
|
||||
func (s *Server) serve(ln net.Listener) error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
staticSub, _ := fs.Sub(staticFS, "static")
|
||||
@@ -417,6 +425,9 @@ func (s *Server) Run() error {
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
IdleTimeout: 30 * time.Minute,
|
||||
}
|
||||
if ln != nil {
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
@@ -3008,7 +3019,11 @@ func (s *Server) persistScanResultsToList(healthy []string) {
|
||||
}
|
||||
list := findList(pl, pl.SelectedList)
|
||||
if list == nil {
|
||||
return
|
||||
// First scan with no lists yet — seed a Default list so the
|
||||
// UI doesn't show empty after the very first scan completes.
|
||||
pl.ActiveLists = append(pl.ActiveLists, ActiveList{Name: defaultListName})
|
||||
list = &pl.ActiveLists[len(pl.ActiveLists)-1]
|
||||
pl.SelectedList = defaultListName
|
||||
}
|
||||
// Don't shrink a populated list on routine periodic checks.
|
||||
if !overwrite && len(list.Resolvers) > 0 {
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
E1000000000000000000E001 /* ThefeedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D001 /* ThefeedApp.swift */; };
|
||||
E1000000000000000000E002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D002 /* ContentView.swift */; };
|
||||
E1000000000000000000E003 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D003 /* WebView.swift */; };
|
||||
E1000000000000000000E004 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D004 /* Bridge.swift */; };
|
||||
E1000000000000000000E005 /* ServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D005 /* ServerController.swift */; };
|
||||
E1000000000000000000E006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D007 /* Assets.xcassets */; };
|
||||
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
|
||||
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
E1000000000000000000E009 /* ThefeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D010 /* ThefeedTests.swift */; };
|
||||
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
|
||||
E1000000000000000000E011 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D012 /* LanguagePicker.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
B1000000000000000000B005 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D1000000000000000000D001 /* ThefeedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedApp.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D003 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D004 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D005 /* ServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerController.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D006 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D1000000000000000000D007 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D1000000000000000000D008 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = Mobile.xcframework; sourceTree = "<group>"; };
|
||||
D1000000000000000000D009 /* Thefeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thefeed.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1000000000000000000D010 /* ThefeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedTests.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D011 /* ThefeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThefeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1000000000000000000D012 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
B1000000000000000000B003 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B1000000000000000000B008 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
A1000000000000000000A002 /* Thefeed (root) */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1000000000000000000A004 /* Thefeed */,
|
||||
A1000000000000000000A006 /* ThefeedTests */,
|
||||
A1000000000000000000A005 /* Frameworks */,
|
||||
A1000000000000000000A003 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A003 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D009 /* Thefeed.app */,
|
||||
D1000000000000000000D011 /* ThefeedTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A004 /* Thefeed */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D001 /* ThefeedApp.swift */,
|
||||
D1000000000000000000D002 /* ContentView.swift */,
|
||||
D1000000000000000000D003 /* WebView.swift */,
|
||||
D1000000000000000000D004 /* Bridge.swift */,
|
||||
D1000000000000000000D005 /* ServerController.swift */,
|
||||
D1000000000000000000D012 /* LanguagePicker.swift */,
|
||||
D1000000000000000000D007 /* Assets.xcassets */,
|
||||
D1000000000000000000D006 /* Info.plist */,
|
||||
);
|
||||
path = Thefeed;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A006 /* ThefeedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D010 /* ThefeedTests.swift */,
|
||||
);
|
||||
path = ThefeedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A005 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D008 /* Mobile.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
B1000000000000000000B001 /* Thefeed */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */;
|
||||
buildPhases = (
|
||||
B1000000000000000000B002 /* Sources */,
|
||||
B1000000000000000000B003 /* Frameworks */,
|
||||
B1000000000000000000B004 /* Resources */,
|
||||
B1000000000000000000B005 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Thefeed;
|
||||
productName = Thefeed;
|
||||
productReference = D1000000000000000000D009 /* Thefeed.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
B1000000000000000000B006 /* ThefeedTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */;
|
||||
buildPhases = (
|
||||
B1000000000000000000B007 /* Sources */,
|
||||
B1000000000000000000B008 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
F1000000000000000000F001 /* PBXTargetDependency */,
|
||||
);
|
||||
name = ThefeedTests;
|
||||
productName = ThefeedTests;
|
||||
productReference = D1000000000000000000D011 /* ThefeedTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
F1000000000000000000F001 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = B1000000000000000000B001 /* Thefeed */;
|
||||
targetProxy = F1000000000000000000F002 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
F1000000000000000000F002 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A1000000000000000000A001 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = B1000000000000000000B001;
|
||||
remoteInfo = Thefeed;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
A1000000000000000000A001 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1530;
|
||||
TargetAttributes = {
|
||||
B1000000000000000000B001 = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
};
|
||||
B1000000000000000000B006 = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
TestTargetID = B1000000000000000000B001;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = A1000000000000000000A002;
|
||||
productRefGroup = A1000000000000000000A003 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
B1000000000000000000B001 /* Thefeed */,
|
||||
B1000000000000000000B006 /* ThefeedTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
B1000000000000000000B004 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E006 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
B1000000000000000000B002 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E001 /* ThefeedApp.swift in Sources */,
|
||||
E1000000000000000000E002 /* ContentView.swift in Sources */,
|
||||
E1000000000000000000E003 /* WebView.swift in Sources */,
|
||||
E1000000000000000000E004 /* Bridge.swift in Sources */,
|
||||
E1000000000000000000E005 /* ServerController.swift in Sources */,
|
||||
E1000000000000000000E011 /* LanguagePicker.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B1000000000000000000B007 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E009 /* ThefeedTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
C1000000000000000000C003 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C004 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C1000000000000000000C005 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Thefeed/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C006 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Thefeed/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C1000000000000000000C008 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C009 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C003 /* Debug */,
|
||||
C1000000000000000000C004 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C005 /* Debug */,
|
||||
C1000000000000000000C006 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C008 /* Debug */,
|
||||
C1000000000000000000C009 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = A1000000000000000000A001 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
E1000000000000000000E001 /* ThefeedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D001 /* ThefeedApp.swift */; };
|
||||
E1000000000000000000E002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D002 /* ContentView.swift */; };
|
||||
E1000000000000000000E003 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D003 /* WebView.swift */; };
|
||||
E1000000000000000000E004 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D004 /* Bridge.swift */; };
|
||||
E1000000000000000000E005 /* ServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D005 /* ServerController.swift */; };
|
||||
E1000000000000000000E006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D007 /* Assets.xcassets */; };
|
||||
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
|
||||
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
E1000000000000000000E009 /* ThefeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D010 /* ThefeedTests.swift */; };
|
||||
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
|
||||
E1000000000000000000E011 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D012 /* LanguagePicker.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
B1000000000000000000B005 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D1000000000000000000D001 /* ThefeedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedApp.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D003 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D004 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D005 /* ServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerController.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D006 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D1000000000000000000D007 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D1000000000000000000D008 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = Mobile.xcframework; sourceTree = "<group>"; };
|
||||
D1000000000000000000D009 /* Thefeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thefeed.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1000000000000000000D010 /* ThefeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedTests.swift; sourceTree = "<group>"; };
|
||||
D1000000000000000000D011 /* ThefeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThefeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1000000000000000000D012 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
B1000000000000000000B003 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B1000000000000000000B008 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
A1000000000000000000A002 /* Thefeed (root) */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1000000000000000000A004 /* Thefeed */,
|
||||
A1000000000000000000A006 /* ThefeedTests */,
|
||||
A1000000000000000000A005 /* Frameworks */,
|
||||
A1000000000000000000A003 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A003 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D009 /* Thefeed.app */,
|
||||
D1000000000000000000D011 /* ThefeedTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A004 /* Thefeed */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D001 /* ThefeedApp.swift */,
|
||||
D1000000000000000000D002 /* ContentView.swift */,
|
||||
D1000000000000000000D003 /* WebView.swift */,
|
||||
D1000000000000000000D004 /* Bridge.swift */,
|
||||
D1000000000000000000D005 /* ServerController.swift */,
|
||||
D1000000000000000000D012 /* LanguagePicker.swift */,
|
||||
D1000000000000000000D007 /* Assets.xcassets */,
|
||||
D1000000000000000000D006 /* Info.plist */,
|
||||
);
|
||||
path = Thefeed;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A006 /* ThefeedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D010 /* ThefeedTests.swift */,
|
||||
);
|
||||
path = ThefeedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1000000000000000000A005 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1000000000000000000D008 /* Mobile.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
B1000000000000000000B001 /* Thefeed */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */;
|
||||
buildPhases = (
|
||||
B1000000000000000000B002 /* Sources */,
|
||||
B1000000000000000000B003 /* Frameworks */,
|
||||
B1000000000000000000B004 /* Resources */,
|
||||
B1000000000000000000B005 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Thefeed;
|
||||
productName = Thefeed;
|
||||
productReference = D1000000000000000000D009 /* Thefeed.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
B1000000000000000000B006 /* ThefeedTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */;
|
||||
buildPhases = (
|
||||
B1000000000000000000B007 /* Sources */,
|
||||
B1000000000000000000B008 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
F1000000000000000000F001 /* PBXTargetDependency */,
|
||||
);
|
||||
name = ThefeedTests;
|
||||
productName = ThefeedTests;
|
||||
productReference = D1000000000000000000D011 /* ThefeedTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
F1000000000000000000F001 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = B1000000000000000000B001 /* Thefeed */;
|
||||
targetProxy = F1000000000000000000F002 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
F1000000000000000000F002 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A1000000000000000000A001 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = B1000000000000000000B001;
|
||||
remoteInfo = Thefeed;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
A1000000000000000000A001 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1530;
|
||||
TargetAttributes = {
|
||||
B1000000000000000000B001 = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
};
|
||||
B1000000000000000000B006 = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
TestTargetID = B1000000000000000000B001;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = A1000000000000000000A002;
|
||||
productRefGroup = A1000000000000000000A003 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
B1000000000000000000B001 /* Thefeed */,
|
||||
B1000000000000000000B006 /* ThefeedTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
B1000000000000000000B004 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E006 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
B1000000000000000000B002 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E001 /* ThefeedApp.swift in Sources */,
|
||||
E1000000000000000000E002 /* ContentView.swift in Sources */,
|
||||
E1000000000000000000E003 /* WebView.swift in Sources */,
|
||||
E1000000000000000000E004 /* Bridge.swift in Sources */,
|
||||
E1000000000000000000E005 /* ServerController.swift in Sources */,
|
||||
E1000000000000000000E011 /* LanguagePicker.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B1000000000000000000B007 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1000000000000000000E009 /* ThefeedTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
C1000000000000000000C003 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C004 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C1000000000000000000C005 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Thefeed/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C006 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Thefeed/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C1000000000000000000C008 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C1000000000000000000C009 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C003 /* Debug */,
|
||||
C1000000000000000000C004 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C005 /* Debug */,
|
||||
C1000000000000000000C006 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C1000000000000000000C008 /* Debug */,
|
||||
C1000000000000000000C009 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = A1000000000000000000A001 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1530"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B1000000000000000000B001"
|
||||
BuildableName = "Thefeed.app"
|
||||
BlueprintName = "Thefeed"
|
||||
ReferencedContainer = "container:Thefeed.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B1000000000000000000B006"
|
||||
BuildableName = "ThefeedTests.xctest"
|
||||
BlueprintName = "ThefeedTests"
|
||||
ReferencedContainer = "container:Thefeed.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B1000000000000000000B006"
|
||||
BuildableName = "ThefeedTests.xctest"
|
||||
BlueprintName = "ThefeedTests"
|
||||
ReferencedContainer = "container:Thefeed.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</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 = "B1000000000000000000B001"
|
||||
BuildableName = "Thefeed.app"
|
||||
BlueprintName = "Thefeed"
|
||||
ReferencedContainer = "container:Thefeed.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B1000000000000000000B001"
|
||||
BuildableName = "Thefeed.app"
|
||||
BlueprintName = "Thefeed"
|
||||
ReferencedContainer = "container:Thefeed.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "image.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 619 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import Foundation
|
||||
import Photos
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
/// Receives WKScriptMessage actions from `window.IOS.*` and routes
|
||||
/// outbound navigations: loopback stays in the WebView, anything else
|
||||
/// hands off to Safari.
|
||||
final class Bridge: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
|
||||
weak var webView: WKWebView?
|
||||
|
||||
func userContentController(
|
||||
_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage
|
||||
) {
|
||||
guard
|
||||
let body = message.body as? [String: Any],
|
||||
let action = body["action"] as? String
|
||||
else { return }
|
||||
|
||||
switch action {
|
||||
case "saveMedia": save(body)
|
||||
case "shareMedia": share(body)
|
||||
case "openMedia": share(body) // iOS treats open and share via the same picker
|
||||
case "setLang": setLang(body)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
||||
) {
|
||||
guard let url = navigationAction.request.url else {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
// Keep loopback inside the WebView; everything else goes to Safari.
|
||||
if let host = url.host, host == "127.0.0.1" || host == "localhost" {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
if navigationAction.navigationType == .linkActivated || url.scheme == "https" || url.scheme == "http" {
|
||||
UIApplication.shared.open(url)
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
private func save(_ body: [String: Any]) {
|
||||
guard let url = decode(body) else { return }
|
||||
let mime = (body["mime"] as? String) ?? ""
|
||||
if mime.hasPrefix("image/") {
|
||||
saveImage(at: url)
|
||||
return
|
||||
}
|
||||
if mime.hasPrefix("video/") {
|
||||
saveVideo(at: url)
|
||||
return
|
||||
}
|
||||
// Fallback for non-media (PDFs, archives, etc.) — share sheet so
|
||||
// the user picks Files / a third-party app.
|
||||
present(url: url, save: false)
|
||||
}
|
||||
|
||||
private func share(_ body: [String: Any]) {
|
||||
guard let url = decode(body) else { return }
|
||||
present(url: url, save: false)
|
||||
}
|
||||
|
||||
// MARK: - Save to Photos
|
||||
|
||||
private func saveImage(at url: URL) {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let image = UIImage(data: data) else {
|
||||
toast("Save failed: cannot decode image")
|
||||
return
|
||||
}
|
||||
requestPhotoAdd { [weak self] granted in
|
||||
guard granted else { self?.toast("Photo library access denied"); return }
|
||||
UIImageWriteToSavedPhotosAlbum(image, self, #selector(Bridge.didFinishSavingImage(_:didFinishSavingWithError:contextInfo:)), nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveVideo(at url: URL) {
|
||||
requestPhotoAdd { [weak self] granted in
|
||||
guard granted else { self?.toast("Photo library access denied"); return }
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
|
||||
}, completionHandler: { ok, err in
|
||||
DispatchQueue.main.async {
|
||||
self?.toast(ok ? "Saved to Photos" : "Save failed: \(err?.localizedDescription ?? "unknown")")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func didFinishSavingImage(
|
||||
_ image: UIImage,
|
||||
didFinishSavingWithError error: NSError?,
|
||||
contextInfo: UnsafeRawPointer
|
||||
) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.toast(error == nil ? "Saved to Photos" : "Save failed: \(error!.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func requestPhotoAdd(_ handler: @escaping (Bool) -> Void) {
|
||||
let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
|
||||
if status == .authorized || status == .limited {
|
||||
handler(true); return
|
||||
}
|
||||
if status == .denied || status == .restricted {
|
||||
handler(false); return
|
||||
}
|
||||
PHPhotoLibrary.requestAuthorization(for: .addOnly) { st in
|
||||
DispatchQueue.main.async { handler(st == .authorized || st == .limited) }
|
||||
}
|
||||
}
|
||||
|
||||
private func toast(_ msg: String) {
|
||||
webView?.evaluateJavaScript(
|
||||
"window.showToast && window.showToast(\(jsString(msg)))",
|
||||
completionHandler: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func jsString(_ s: String) -> String {
|
||||
let escaped = s
|
||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||
.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
return "\"\(escaped)\""
|
||||
}
|
||||
|
||||
// MARK: - Language
|
||||
|
||||
private func setLang(_ body: [String: Any]) {
|
||||
guard let lang = body["lang"] as? String else { return }
|
||||
UserDefaults.standard.set(lang, forKey: "tf.lang")
|
||||
}
|
||||
|
||||
private func decode(_ body: [String: Any]) -> URL? {
|
||||
guard
|
||||
let b64 = body["body"] as? String,
|
||||
let data = Data(base64Encoded: b64),
|
||||
let name = (body["name"] as? String).flatMap(safeName)
|
||||
else { return nil }
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("share", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(
|
||||
at: dir, withIntermediateDirectories: true
|
||||
)
|
||||
let url = dir.appendingPathComponent(name)
|
||||
do {
|
||||
try data.write(to: url, options: .atomic)
|
||||
return url
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func present(url: URL, save: Bool) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard
|
||||
let scene = UIApplication.shared.connectedScenes
|
||||
.compactMap({ $0 as? UIWindowScene })
|
||||
.first,
|
||||
let root = scene.windows.first?.rootViewController
|
||||
else { return }
|
||||
let activities: [UIActivity]? = nil
|
||||
let vc = UIActivityViewController(
|
||||
activityItems: [url],
|
||||
applicationActivities: activities
|
||||
)
|
||||
// iPad popover anchor.
|
||||
vc.popoverPresentationController?.sourceView = self?.webView
|
||||
root.present(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func safeName(_ s: String) -> String? {
|
||||
let bad = CharacterSet(charactersIn: "/\\:*?\"<>|\0")
|
||||
let cleaned = s.components(separatedBy: bad).joined(separator: "_")
|
||||
return cleaned.isEmpty ? nil : cleaned
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var server: ServerController
|
||||
@AppStorage("tf.lang") private var lang: String = ""
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
if !lang.isEmpty && server.port > 0 {
|
||||
WebView(url: URL(string: "http://127.0.0.1:\(server.port)")!)
|
||||
.ignoresSafeArea()
|
||||
} else if let err = server.lastError {
|
||||
VStack(spacing: 12) {
|
||||
Text("startup failed").font(.headline).foregroundColor(.white)
|
||||
Text(err).font(.caption).foregroundColor(.secondary)
|
||||
Button("retry") { server.start() }
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>TheFeed</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!-- Loopback HTTP for the embedded server. NSAllowsLocalNetworking
|
||||
keeps cleartext blocked for everything else. -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<!-- Required for saving downloaded images/videos to Photos. -->
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>thefeed needs to save downloaded images and videos to your Photos library.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,68 @@
|
||||
import SwiftUI
|
||||
|
||||
/// First-launch language selection. Stored in UserDefaults under "tf.lang".
|
||||
struct LanguagePickerView: View {
|
||||
let onPick: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(red: 0.07, green: 0.09, blue: 0.13).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 40) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("TheFeed")
|
||||
.font(.system(size: 40, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
Text("Choose your language · زبان خود را انتخاب کنید")
|
||||
.font(.callout)
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
VStack(spacing: 16) {
|
||||
LanguageButton(label: "English", subtitle: "Continue in English") {
|
||||
onPick("en")
|
||||
}
|
||||
LanguageButton(label: "فارسی", subtitle: "ادامه به زبان فارسی") {
|
||||
onPick("fa")
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct LanguageButton: View {
|
||||
let label: String
|
||||
let subtitle: String
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: 4) {
|
||||
Text(label)
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Text(subtitle)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color.white.opacity(0.08))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(Color.white.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
import Mobile
|
||||
import UIKit
|
||||
|
||||
/// Owns the embedded gomobile-backed HTTP server.
|
||||
/// Restarts on foreground and stops on background since iOS doesn't
|
||||
/// permit a long-lived background server.
|
||||
final class ServerController: ObservableObject {
|
||||
@Published private(set) var port: Int = 0
|
||||
@Published private(set) var lastError: String?
|
||||
|
||||
private var instance: MobileServer?
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
|
||||
init() {
|
||||
let center = NotificationCenter.default
|
||||
observers.append(center.addObserver(
|
||||
forName: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil, queue: .main
|
||||
) { [weak self] _ in self?.stop() })
|
||||
observers.append(center.addObserver(
|
||||
forName: UIApplication.willEnterForegroundNotification,
|
||||
object: nil, queue: .main
|
||||
) { [weak self] _ in self?.start() })
|
||||
}
|
||||
|
||||
deinit {
|
||||
observers.forEach(NotificationCenter.default.removeObserver)
|
||||
instance?.stop()
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard instance == nil else { return }
|
||||
do {
|
||||
let dir = try Self.dataDir()
|
||||
var err: NSError?
|
||||
guard let s = MobileNewServer(dir.path, &err) else {
|
||||
lastError = err?.localizedDescription ?? "server start failed"
|
||||
return
|
||||
}
|
||||
instance = s
|
||||
port = Int(s.port())
|
||||
lastError = nil
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
instance?.stop()
|
||||
instance = nil
|
||||
port = 0
|
||||
}
|
||||
|
||||
private static func dataDir() throws -> URL {
|
||||
let docs = try FileManager.default.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true
|
||||
)
|
||||
let dir = docs.appendingPathComponent("thefeeddata", isDirectory: true)
|
||||
try FileManager.default.createDirectory(
|
||||
at: dir, withIntermediateDirectories: true
|
||||
)
|
||||
return dir
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct ThefeedApp: App {
|
||||
@StateObject private var server = ServerController()
|
||||
@AppStorage("tf.lang") private var lang: String = ""
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(server)
|
||||
.onAppear { server.start() }
|
||||
.onDisappear { server.stop() }
|
||||
.fullScreenCover(isPresented: Binding(
|
||||
get: { lang.isEmpty },
|
||||
set: { _ in }
|
||||
)) {
|
||||
LanguagePickerView { picked in
|
||||
lang = picked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct WebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeCoordinator() -> Bridge { Bridge() }
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let cfg = WKWebViewConfiguration()
|
||||
cfg.websiteDataStore = .default()
|
||||
cfg.allowsInlineMediaPlayback = true
|
||||
cfg.mediaTypesRequiringUserActionForPlayback = []
|
||||
|
||||
let userContent = WKUserContentController()
|
||||
userContent.add(context.coordinator, name: "thefeed")
|
||||
cfg.userContentController = userContent
|
||||
|
||||
let view = WKWebView(frame: .zero, configuration: cfg)
|
||||
view.allowsBackForwardNavigationGestures = true
|
||||
view.scrollView.bounces = true
|
||||
view.navigationDelegate = context.coordinator
|
||||
context.coordinator.webView = view
|
||||
|
||||
userContent.addUserScript(WKUserScript(
|
||||
source: shimSource(),
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
))
|
||||
|
||||
view.load(URLRequest(url: url))
|
||||
return view
|
||||
}
|
||||
|
||||
private func shimSource() -> String {
|
||||
let lang = resolveLang()
|
||||
let langJS = lang.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
return """
|
||||
window.IOS = {
|
||||
isIOS: true,
|
||||
_lang: "\(langJS)",
|
||||
getLang: function() { return this._lang; },
|
||||
setLang: function(l) {
|
||||
this._lang = l;
|
||||
window.webkit.messageHandlers.thefeed.postMessage(
|
||||
{ action: 'setLang', lang: l });
|
||||
},
|
||||
saveMedia: function(b64, mime, name) {
|
||||
window.webkit.messageHandlers.thefeed.postMessage(
|
||||
{ action: 'saveMedia', body: b64, mime: mime, name: name });
|
||||
},
|
||||
shareMedia: function(b64, mime, name) {
|
||||
window.webkit.messageHandlers.thefeed.postMessage(
|
||||
{ action: 'shareMedia', body: b64, mime: mime, name: name });
|
||||
},
|
||||
openMedia: function(b64, mime, name) {
|
||||
window.webkit.messageHandlers.thefeed.postMessage(
|
||||
{ action: 'openMedia', body: b64, mime: mime, name: name });
|
||||
}
|
||||
};
|
||||
"""
|
||||
}
|
||||
|
||||
/// Reads the language picked at first launch. Falls back to "en"
|
||||
/// for the brief window before the picker has been answered.
|
||||
private func resolveLang() -> String {
|
||||
let saved = UserDefaults.standard.string(forKey: "tf.lang") ?? ""
|
||||
return saved.isEmpty ? "en" : saved
|
||||
}
|
||||
|
||||
func updateUIView(_ view: WKWebView, context: Context) {
|
||||
if view.url != url {
|
||||
view.load(URLRequest(url: url))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import XCTest
|
||||
import Mobile
|
||||
|
||||
final class ThefeedTests: XCTestCase {
|
||||
func testRejectsEmptyDir() {
|
||||
var err: NSError?
|
||||
let s = MobileNewServer("", &err)
|
||||
XCTAssertNil(s)
|
||||
XCTAssertNotNil(err)
|
||||
}
|
||||
|
||||
func testServeAndStop() throws {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("thefeed-test-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(
|
||||
at: dir, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: dir) }
|
||||
|
||||
var err: NSError?
|
||||
guard let server = MobileNewServer(dir.path, &err) else {
|
||||
XCTFail("start failed: \(err?.localizedDescription ?? "?")")
|
||||
return
|
||||
}
|
||||
XCTAssertGreaterThan(server.port(), 0)
|
||||
|
||||
let url = URL(string: "http://127.0.0.1:\(server.port())/api/status")!
|
||||
let exp = expectation(description: "status")
|
||||
let task = URLSession.shared.dataTask(with: url) { _, resp, _ in
|
||||
if let http = resp as? HTTPURLResponse {
|
||||
XCTAssertEqual(http.statusCode, 200)
|
||||
}
|
||||
exp.fulfill()
|
||||
}
|
||||
task.resume()
|
||||
wait(for: [exp], timeout: 10)
|
||||
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Package mobile is the gomobile-bind entry point used by the iOS
|
||||
// (and Mac Catalyst) Swift app. It wraps internal/web.Server so the
|
||||
// HTTP server runs in-process — iOS does not allow bundled child
|
||||
// executables, so the Android subprocess approach can't be reused.
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sartoopjj/thefeed/internal/web"
|
||||
)
|
||||
|
||||
// Server is a running thefeed-client instance bound to 127.0.0.1.
|
||||
type Server struct {
|
||||
web *web.Server
|
||||
ln net.Listener
|
||||
port int
|
||||
|
||||
mu sync.Mutex
|
||||
stopped bool
|
||||
doneErr error
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewServer starts a server on a kernel-assigned port. dataDir must be
|
||||
// a writable, app-private directory (e.g. NSDocumentDirectory on iOS).
|
||||
func NewServer(dataDir string) (*Server, error) {
|
||||
if dataDir == "" {
|
||||
return nil, errors.New("mobile: dataDir is empty")
|
||||
}
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
ws, err := web.New(dataDir, port, "127.0.0.1", "")
|
||||
if err != nil {
|
||||
_ = ln.Close()
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{
|
||||
web: ws,
|
||||
ln: ln,
|
||||
port: port,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
err := ws.Serve(ln)
|
||||
s.mu.Lock()
|
||||
s.doneErr = err
|
||||
s.mu.Unlock()
|
||||
close(s.done)
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Port returns the listening port (0 after Stop).
|
||||
func (s *Server) Port() int {
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
return s.port
|
||||
}
|
||||
|
||||
// Stop closes the listener and waits for serve to return.
|
||||
func (s *Server) Stop() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.stopped {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.stopped = true
|
||||
s.mu.Unlock()
|
||||
_ = s.ln.Close()
|
||||
<-s.done
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewServerEmptyDir(t *testing.T) {
|
||||
if _, err := NewServer(""); err == nil {
|
||||
t.Errorf("NewServer(\"\") succeeded, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerLifecycle(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s, err := NewServer(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
defer s.Stop()
|
||||
|
||||
if s.Port() <= 0 {
|
||||
t.Fatalf("Port() = %d, want > 0", s.Port())
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", s.Port())
|
||||
resp, err := pollGet(url, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "version") {
|
||||
t.Errorf("body = %q, expected to contain 'version'", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopIsIdempotent(t *testing.T) {
|
||||
s, err := NewServer(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
s.Stop()
|
||||
s.Stop() // must not panic
|
||||
}
|
||||
|
||||
func TestStopReleasesPort(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s, err := NewServer(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", s.Port())
|
||||
if _, err := pollGet(url, 3*time.Second); err != nil {
|
||||
t.Fatalf("server never came up: %v", err)
|
||||
}
|
||||
s.Stop()
|
||||
|
||||
// After Stop the listener is closed — a fresh request must fail.
|
||||
c := http.Client{Timeout: 500 * time.Millisecond}
|
||||
if resp, err := c.Get(url); err == nil {
|
||||
resp.Body.Close()
|
||||
t.Errorf("server still answering after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
// pollGet retries until the server is up, since the Serve goroutine
|
||||
// may take a moment to start accepting on the listener.
|
||||
func pollGet(url string, total time.Duration) (*http.Response, error) {
|
||||
deadline := time.Now().Add(total)
|
||||
c := http.Client{Timeout: 500 * time.Millisecond}
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := c.Get(url)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build gomobiletools
|
||||
|
||||
package mobile
|
||||
|
||||
// Pins golang.org/x/mobile/bind* in go.mod so `go mod tidy` doesn't
|
||||
// drop them between gomobile-bind invocations. The build tag keeps
|
||||
// these out of normal compilation; the imports exist solely for go
|
||||
// modules' dependency graph.
|
||||
import (
|
||||
_ "golang.org/x/mobile/bind"
|
||||
_ "golang.org/x/mobile/bind/objc"
|
||||
)
|
||||
Reference in New Issue
Block a user