mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 04:54:34 +03:00
feat: implement first-launch language picker and remove deprecated language selection view
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
name: iOS Release (preview)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag for the pre-release (e.g. v0.15.0-ios.1)'
|
||||
required: true
|
||||
default: 'v0.0.0-ios.0'
|
||||
push:
|
||||
tags:
|
||||
- 'v*-ios.*'
|
||||
- 'v*-ios-*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
cache: true
|
||||
|
||||
- name: Select Xcode
|
||||
run: sudo xcode-select -s /Applications/Xcode.app
|
||||
|
||||
- name: Install gomobile + gobind
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go install golang.org/x/mobile/cmd/gobind@latest
|
||||
gomobile init
|
||||
go get golang.org/x/mobile/bind golang.org/x/mobile/bind/objc
|
||||
go mod tidy
|
||||
|
||||
- name: Build Mobile.xcframework
|
||||
run: gomobile bind -iosversion=14.0 -target=ios,iossimulator -o ios/Mobile.xcframework ./mobile
|
||||
|
||||
- name: Archive (unsigned)
|
||||
run: |
|
||||
xcodebuild \
|
||||
-project ios/Thefeed.xcodeproj \
|
||||
-scheme Thefeed \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath build/Thefeed.xcarchive \
|
||||
archive \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=""
|
||||
|
||||
- name: Pack unsigned IPA
|
||||
run: |
|
||||
mkdir -p build/Payload
|
||||
cp -r build/Thefeed.xcarchive/Products/Applications/Thefeed.app build/Payload/
|
||||
(cd build && zip -qry "Thefeed-${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}-unsigned.ipa" Payload)
|
||||
ls -lh build/*.ipa
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
if [ -z "$TAG" ] || [ "$TAG" = "main" ]; then TAG="${{ github.event.inputs.tag }}"; fi
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create pre-release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.tag }} (iOS preview)
|
||||
prerelease: true
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
⚠️ **iOS-only preview release.**
|
||||
|
||||
This release ships only the iOS build. Other platforms
|
||||
(server, Linux/macOS/Windows clients, Android APKs) are not
|
||||
included — pull the latest stable tag for those.
|
||||
|
||||
The IPA is **unsigned**. To install:
|
||||
- Re-sign with your own provisioning profile and distribute via TestFlight, or
|
||||
- Use a sideloading tool such as AltStore / Sideloadly with your own Apple ID.
|
||||
|
||||
iOS 14.0 or newer required. Universal binary (iPhone + iPad).
|
||||
files: |
|
||||
build/Thefeed-*.ipa
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -2940,6 +2940,51 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- First-launch language picker. Visible until lang is set; pure HTML/JS
|
||||
so it works on iOS, Android, and the browser without native code. -->
|
||||
<div id="firstRunLangModal" style="display:none;position:fixed;inset:0;z-index:99999;background:#0f1722;color:#fff;align-items:center;justify-content:center;flex-direction:column;padding:24px;font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif">
|
||||
<div style="font-size:38px;font-weight:700;margin-bottom:6px;letter-spacing:-0.5px">TheFeed</div>
|
||||
<div style="opacity:.6;font-size:14px;margin-bottom:36px;text-align:center">Choose your language · زبان خود را انتخاب کنید</div>
|
||||
<div style="display:flex;flex-direction:column;gap:14px;width:100%;max-width:320px">
|
||||
<button onclick="firstRunPickLang('en')" style="background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:14px;padding:18px;font-size:18px;font-weight:600;cursor:pointer">English<div style="font-size:12px;font-weight:400;opacity:.7;margin-top:2px">Continue in English</div></button>
|
||||
<button onclick="firstRunPickLang('fa')" style="background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:14px;padding:18px;font-size:18px;font-weight:600;cursor:pointer">فارسی<div style="font-size:12px;font-weight:400;opacity:.7;margin-top:2px">ادامه به زبان فارسی</div></button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
// Server is the source of truth: wiping thefeeddata/ resets lang
|
||||
// along with everything else. Sync XHR keeps this decision before
|
||||
// first paint so the modal never flashes for returning users.
|
||||
var lang = '';
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/settings', false);
|
||||
xhr.send();
|
||||
if (xhr.status === 200) lang = (JSON.parse(xhr.responseText).lang || '');
|
||||
} catch (e) {
|
||||
// Server unreachable — fall back to client-side caches.
|
||||
try { if (typeof IOS !== 'undefined' && IOS.getLang) lang = IOS.getLang() || ''; } catch (e2) { }
|
||||
try { if (!lang && typeof Android !== 'undefined' && Android.getLang) lang = Android.getLang() || ''; } catch (e2) { }
|
||||
if (!lang) lang = localStorage.getItem('thefeed_lang') || '';
|
||||
}
|
||||
if (!lang) document.getElementById('firstRunLangModal').style.display = 'flex';
|
||||
})();
|
||||
function firstRunPickLang(l) {
|
||||
// Persist server-side synchronously so the reload sees it.
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/settings', false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify({ lang: l }));
|
||||
} catch (e) { }
|
||||
try { localStorage.setItem('thefeed_lang', l); } catch (e) { }
|
||||
try { if (typeof IOS !== 'undefined' && IOS.setLang) IOS.setLang(l); } catch (e) { }
|
||||
try { if (typeof Android !== 'undefined' && Android.setLang) Android.setLang(l); } catch (e) { }
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app" id="app">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
@@ -4043,9 +4088,21 @@
|
||||
// native value first survives an embedded server rebinding to a
|
||||
// new loopback port on each launch.
|
||||
var lang = (function () {
|
||||
// Same precedence as the modal-init script: server first (so a
|
||||
// wiped thefeeddata/ also wipes the language), then native bridge,
|
||||
// then localStorage as a final cache.
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/settings', false);
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
var d = JSON.parse(xhr.responseText);
|
||||
if (d.lang) return d.lang;
|
||||
}
|
||||
} catch (e) { }
|
||||
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';
|
||||
return localStorage.getItem('thefeed_lang') || '';
|
||||
})();
|
||||
function t(k) { return (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k }
|
||||
function applyLang() {
|
||||
@@ -4055,8 +4112,8 @@
|
||||
document.querySelectorAll('[data-i18n]').forEach(function (el) { el.textContent = t(el.dataset.i18n) });
|
||||
document.querySelectorAll('[data-i18n-ph]').forEach(function (el) { el.placeholder = t(el.dataset.i18nPh) });
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(function (el) { el.title = t(el.dataset.i18nTitle) });
|
||||
document.getElementById('langFa').classList.toggle('active-lang', lang === 'fa');
|
||||
document.getElementById('langEn').classList.toggle('active-lang', lang === 'en');
|
||||
var lf = document.getElementById('langFa'); if (lf) lf.classList.toggle('active-lang', lang === 'fa');
|
||||
var le = document.getElementById('langEn'); if (le) le.classList.toggle('active-lang', lang === 'en');
|
||||
document.getElementById('sendInput').style.direction = isRtl ? 'rtl' : 'ltr';
|
||||
applyThemeButtons();
|
||||
// Re-render dynamic content
|
||||
@@ -5351,9 +5408,15 @@
|
||||
// pre-named profile instead of "thefeed.example.com" pulled
|
||||
// from the domain. Capped at 32 chars on this side; the
|
||||
// import side enforces the same cap defensively.
|
||||
// Drop the default ":53" suffix from each resolver — the parser
|
||||
// assumes 53 when no port is given, so omitting it shortens the
|
||||
// URI considerably. Custom ports are kept and stay un-encoded.
|
||||
var compact = resolvers.map(function (r) {
|
||||
return r.replace(/:53$/, '');
|
||||
}).join(',');
|
||||
var uri = 'thefeed://' + encodeURIComponent(p.config.domain)
|
||||
+ '/' + encodeURIComponent(p.config.key)
|
||||
+ '?r=' + encodeURIComponent(resolvers.join(','));
|
||||
+ '?r=' + encodeURIComponent(compact).replace(/%3A/g, ':');
|
||||
var nick = (p.nickname || '').trim().slice(0, 32);
|
||||
if (nick && nick !== p.config.domain) {
|
||||
uri += '&n=' + encodeURIComponent(nick);
|
||||
|
||||
@@ -2,12 +2,11 @@ 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 {
|
||||
if server.port > 0 {
|
||||
WebView(url: URL(string: "http://127.0.0.1:\(server.port)")!)
|
||||
.ignoresSafeArea()
|
||||
} else if let err = server.lastError {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import SwiftUI
|
||||
@main
|
||||
struct ThefeedApp: App {
|
||||
@StateObject private var server = ServerController()
|
||||
@AppStorage("tf.lang") private var lang: String = ""
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
@@ -11,14 +10,6 @@ struct ThefeedApp: App {
|
||||
.environmentObject(server)
|
||||
.onAppear { server.start() }
|
||||
.onDisappear { server.stop() }
|
||||
.fullScreenCover(isPresented: Binding(
|
||||
get: { lang.isEmpty },
|
||||
set: { _ in }
|
||||
)) {
|
||||
LanguagePickerView { picked in
|
||||
lang = picked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,10 @@ struct WebView: UIViewRepresentable {
|
||||
"""
|
||||
}
|
||||
|
||||
/// Reads the language picked at first launch. Falls back to "en"
|
||||
/// for the brief window before the picker has been answered.
|
||||
/// Returns the saved language ("" on first launch) so the WebView's
|
||||
/// own language picker can detect first-launch and show its modal.
|
||||
private func resolveLang() -> String {
|
||||
let saved = UserDefaults.standard.string(forKey: "tf.lang") ?? ""
|
||||
return saved.isEmpty ? "en" : saved
|
||||
return UserDefaults.standard.string(forKey: "tf.lang") ?? ""
|
||||
}
|
||||
|
||||
func updateUIView(_ view: WKWebView, context: Context) {
|
||||
|
||||
Reference in New Issue
Block a user