Skip to content

Commit bc75e26

Browse files
committed
Refactor: Modularize project structure, migrate to SwiftData, JWT auth, and xcstrings
1 parent 75d8e96 commit bc75e26

48 files changed

Lines changed: 6875 additions & 2471 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Floraboard iOS CI Pipeline
2+
name: CI
3+
4+
on:
5+
push:
6+
branches: [main, develop]
7+
pull_request:
8+
branches: [main]
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
lint:
16+
name: SwiftLint
17+
runs-on: macos-15
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Install SwiftLint
22+
run: brew install swiftlint
23+
24+
- name: Run SwiftLint
25+
run: swiftlint lint --strict --reporter github-actions-logging
26+
27+
build:
28+
name: Build & Test
29+
runs-on: macos-15
30+
needs: lint
31+
strategy:
32+
matrix:
33+
destination:
34+
- "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.0"
35+
steps:
36+
- uses: actions/checkout@v4
37+
38+
- name: Select Xcode
39+
run: sudo xcode-select -s /Applications/Xcode_16.0.app
40+
41+
- name: Build
42+
run: |
43+
xcodebuild build \
44+
-project Floreboard.xcodeproj \
45+
-scheme Floreboard \
46+
-destination "${{ matrix.destination }}" \
47+
-configuration Debug \
48+
CODE_SIGN_IDENTITY="" \
49+
CODE_SIGNING_REQUIRED=NO \
50+
| xcpretty
51+
52+
- name: Test
53+
run: |
54+
xcodebuild test \
55+
-project Floreboard.xcodeproj \
56+
-scheme Floreboard \
57+
-destination "${{ matrix.destination }}" \
58+
-configuration Debug \
59+
CODE_SIGN_IDENTITY="" \
60+
CODE_SIGNING_REQUIRED=NO \
61+
| xcpretty
62+
continue-on-error: true # Tests may not exist yet

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,21 @@ xcshareddata/
2626
*.moved-aside
2727
*.xcuserstate
2828
*.users
29+
30+
# Swift Package Manager
31+
.build/
32+
Packages/
33+
*.resolved
34+
35+
# macOS
36+
.AppleDouble
37+
.LSOverride
38+
.Spotlight-V100
39+
.Trashes
40+
41+
# SwiftLint
42+
.swiftlint/
43+
44+
# Misc
45+
*.playground/
46+
.gemini/

.swiftlint.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# SwiftLint Configuration for Floraboard iOS
2+
# https://github.com/realm/SwiftLint
3+
4+
included:
5+
- Floreboard
6+
7+
excluded:
8+
- Floreboard/Assets.xcassets
9+
- DerivedData
10+
- build
11+
12+
# Rule Configuration
13+
disabled_rules:
14+
- trailing_whitespace # Too noisy during active development
15+
- todo # Allow TODO/FIXME comments
16+
17+
opt_in_rules:
18+
- closure_spacing
19+
- contains_over_filter_count
20+
- empty_count
21+
- empty_string
22+
- fatal_error_message
23+
- first_where
24+
- force_unwrapping
25+
- implicitly_unwrapped_optional
26+
- last_where
27+
- modifier_order
28+
- overridden_super_call
29+
- private_action
30+
- private_outlet
31+
- redundant_nil_coalescing
32+
- sorted_first_last
33+
- unowned_variable_capture
34+
- vertical_whitespace_closing_braces
35+
36+
# Length Rules
37+
line_length:
38+
warning: 130
39+
error: 200
40+
ignores_comments: true
41+
ignores_urls: true
42+
43+
file_length:
44+
warning: 500
45+
error: 800
46+
47+
function_body_length:
48+
warning: 60
49+
error: 100
50+
51+
type_body_length:
52+
warning: 400
53+
error: 600
54+
55+
# Naming
56+
type_name:
57+
min_length: 3
58+
max_length: 50
59+
60+
identifier_name:
61+
min_length:
62+
warning: 2
63+
error: 1
64+
max_length:
65+
warning: 50
66+
error: 60
67+
excluded:
68+
- id
69+
- i
70+
- x
71+
- y
72+
- p
73+
- s
74+
- c
75+
- ok
76+
77+
# Nesting
78+
nesting:
79+
type_level: 3
80+
function_level: 3
81+
82+
# Reporter
83+
reporter: "xcode"

Floreboard/App/ContentView.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import SwiftUI
2+
3+
struct ContentView: View {
4+
@StateObject private var authService = AuthService.shared
5+
@StateObject private var localizationManager = LocalizationManager.shared
6+
@State private var selection = 0
7+
8+
var body: some View {
9+
Group {
10+
if authService.isAuthenticated {
11+
TabView(selection: $selection) {
12+
HomeView(selection: $selection)
13+
.tabItem {
14+
Label(localizationManager.t("app.nav.dashboard"), systemImage: "square.grid.2x2")
15+
}
16+
.tag(0)
17+
18+
InventoryView()
19+
.tabItem {
20+
Label(localizationManager.t("app.nav.inventory"), systemImage: "leaf.fill")
21+
}
22+
.tag(1)
23+
24+
HistoryView(onStartDesign: {
25+
HapticManager.shared.impact(style: .light)
26+
selection = 3
27+
})
28+
.tabItem {
29+
Label(localizationManager.t("app.nav.history"), systemImage: "clock.arrow.circlepath")
30+
}
31+
.tag(2)
32+
33+
DesignMainView()
34+
.tabItem {
35+
Label(localizationManager.t("app.nav.design"), systemImage: "wand.and.stars")
36+
}
37+
.tag(3)
38+
39+
SettingsView()
40+
.tabItem {
41+
Label(localizationManager.t("app.nav.settings"), systemImage: "gear")
42+
}
43+
.tag(4)
44+
}
45+
} else {
46+
LoginView()
47+
}
48+
}
49+
.tint(AppTheme.primary)
50+
}
51+
}

Floreboard/App/FloreboardApp.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// FloreboardApp.swift
3+
// Floreboard
4+
//
5+
// Created by 陈永林 on 26/01/2026.
6+
//
7+
8+
import SwiftData
9+
import SwiftUI
10+
11+
@main
12+
struct FloreboardApp: App {
13+
let container: ModelContainer
14+
15+
init() {
16+
let schema = Schema([FlowerRecord.self, DesignRecord.self])
17+
let config = ModelConfiguration(isStoredInMemoryOnly: false)
18+
do {
19+
container = try ModelContainer(for: schema, configurations: config)
20+
} catch {
21+
fatalError("Failed to initialize ModelContainer: \(error)")
22+
}
23+
let context = ModelContext(container)
24+
InventoryService.shared.configure(with: context)
25+
HistoryService.shared.configure(with: context)
26+
}
27+
28+
var body: some Scene {
29+
WindowGroup {
30+
ContentView()
31+
.modelContainer(container)
32+
.environmentObject(AuthService.shared)
33+
.environmentObject(InventoryService.shared)
34+
}
35+
}
36+
}

Floreboard/App/Localization.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Localization.swift
3+
// Floreboard
4+
//
5+
// Migrated to String Catalog (.xcstrings) backed localization.
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
enum Language: String, CaseIterable, Identifiable {
12+
case en = "en"
13+
case zh = "zh"
14+
15+
var id: String { rawValue }
16+
17+
var displayName: String {
18+
switch self {
19+
case .en: return "English"
20+
case .zh: return "简体中文"
21+
}
22+
}
23+
24+
/// The lproj directory name used by Apple localization
25+
var lprojName: String {
26+
switch self {
27+
case .en: return "en"
28+
case .zh: return "zh-Hans"
29+
}
30+
}
31+
}
32+
33+
@MainActor
34+
class LocalizationManager: ObservableObject {
35+
static let shared = LocalizationManager()
36+
37+
@Published var currentLanguage: Language {
38+
didSet {
39+
UserDefaults.standard.set(currentLanguage.rawValue, forKey: "app_language")
40+
updateBundle()
41+
}
42+
}
43+
44+
private(set) var localizedBundle: Bundle = .main
45+
46+
private init() {
47+
if let saved = UserDefaults.standard.string(forKey: "app_language"),
48+
let lang = Language(rawValue: saved)
49+
{
50+
self.currentLanguage = lang
51+
} else {
52+
// Default to device language if matches, else en
53+
let deviceLang = Locale.current.language.languageCode?.identifier ?? "en"
54+
self.currentLanguage = deviceLang.contains("zh") ? .zh : .en
55+
}
56+
updateBundle()
57+
}
58+
59+
private func updateBundle() {
60+
if let path = Bundle.main.path(forResource: currentLanguage.lprojName, ofType: "lproj"),
61+
let bundle = Bundle(path: path)
62+
{
63+
localizedBundle = bundle
64+
} else {
65+
localizedBundle = .main
66+
}
67+
}
68+
69+
func t(_ key: String, _ args: [String: String] = [:]) -> String {
70+
var value = localizedBundle.localizedString(forKey: key, value: key, table: nil)
71+
for (k, v) in args {
72+
value = value.replacingOccurrences(of: "{{\(k)}}", with: v)
73+
}
74+
return value
75+
}
76+
}
77+
78+
// Helper for easier access in Views
79+
// Usage: Tx.t("key")
80+
struct Tx {
81+
static func t(_ key: String, _ args: [String: String] = [:]) -> String {
82+
LocalizationManager.shared.t(key, args)
83+
}
84+
}
85+
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
3+
struct ApiConfig: Codable, Identifiable {
4+
var id: String?
5+
var apiKey: String
6+
var endpoint: String
7+
var textModel: String
8+
var visionModel: String
9+
var imageModel: String
10+
var imageEndpoint: String?
11+
var budget: Double
12+
var alertThreshold: Int
13+
var lowStockThreshold: Int
14+
var updatedAt: Double?
15+
16+
static let `default` = ApiConfig(
17+
apiKey: "",
18+
endpoint: "https://floreboard-ai-proxy.cybercorlin.workers.dev",
19+
textModel: "",
20+
visionModel: "",
21+
imageModel: "",
22+
budget: 500,
23+
alertThreshold: 5,
24+
lowStockThreshold: 10
25+
)
26+
}
27+
28+
extension ApiConfig {
29+
static func normalizeEndpoint(_ value: String) -> String {
30+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
31+
guard trimmed != "/" else { return "" }
32+
return trimmed.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
33+
}
34+
35+
mutating func normalizeEndpoints() {
36+
endpoint = Self.normalizeEndpoint(endpoint)
37+
if let imageEndpoint {
38+
let normalizedImageEndpoint = Self.normalizeEndpoint(imageEndpoint)
39+
self.imageEndpoint = normalizedImageEndpoint.isEmpty ? nil : normalizedImageEndpoint
40+
}
41+
}
42+
}
43+
44+
struct UsageRecord: Codable, Identifiable {
45+
var id: String
46+
var date: Double
47+
var tokens: Int
48+
var type: String
49+
var cost: Double
50+
}
51+
52+
struct Tenant: Codable, Identifiable {
53+
var id: String
54+
var name: String
55+
}

0 commit comments

Comments
 (0)