Skip to content

Commit 5f40d9c

Browse files
committed
Merge branch 'main' of isc.github.com:plainhub/plain-app
2 parents 74b778e + 4041b8e commit 5f40d9c

12 files changed

Lines changed: 396 additions & 189 deletions

File tree

iosApp/iosApp.xcodeproj/project.pbxproj

Lines changed: 168 additions & 127 deletions
Large diffs are not rendered by default.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "srgb",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0xFF",
9+
"green" : "0x7A",
10+
"red" : "0x00"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
}
8+
],
9+
"info" : {
10+
"author" : "xcode",
11+
"version" : 1
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}

iosApp/iosApp/ContentView.swift

Lines changed: 0 additions & 18 deletions
This file was deleted.

iosApp/iosApp/Info.plist

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<dict>
55
<key>CFBundleDevelopmentRegion</key>
66
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>PlainApp</string>
79
<key>CFBundleExecutable</key>
810
<string>$(EXECUTABLE_NAME)</string>
911
<key>CFBundleIdentifier</key>
@@ -13,34 +15,42 @@
1315
<key>CFBundleName</key>
1416
<string>$(PRODUCT_NAME)</string>
1517
<key>CFBundlePackageType</key>
16-
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
18+
<string>APPL</string>
1719
<key>CFBundleShortVersionString</key>
18-
<string>$(MARKETING_VERSION)</string>
20+
<string>1.0.0</string>
1921
<key>CFBundleVersion</key>
20-
<string>$(CURRENT_PROJECT_VERSION)</string>
21-
<key>UILaunchScreen</key>
22-
<dict/>
23-
<key>CADisableMinimumFrameDurationOnPhone</key>
22+
<string>1</string>
23+
<key>LSRequiresIPhoneOS</key>
2424
<true/>
25-
<key>NSPhotoLibraryUsageDescription</key>
26-
<string>Plain needs access to your photo library to import and back up images and videos.</string>
27-
<key>NSPhotoLibraryAddUsageDescription</key>
28-
<string>Plain needs access to save images and videos to your photo library.</string>
29-
<key>NSCameraUsageDescription</key>
30-
<string>Plain needs camera access to scan QR codes and capture media.</string>
31-
<key>NSMicrophoneUsageDescription</key>
32-
<string>Plain needs microphone access for screen mirroring audio.</string>
33-
<key>NSContactsUsageDescription</key>
34-
<string>Plain needs contacts access to back up your address book.</string>
35-
<key>NSLocalNetworkUsageDescription</key>
36-
<string>Plain uses your local network to discover and connect to nearby Plain-enabled devices.</string>
37-
<key>NSBonjourServices</key>
25+
<key>UILaunchScreen</key>
26+
<dict>
27+
<key>UIColorName</key>
28+
<string>AccentColor</string>
29+
</dict>
30+
<key>UISupportedInterfaceOrientations</key>
3831
<array>
39-
<string>_plain._tcp</string>
32+
<string>UIInterfaceOrientationPortrait</string>
33+
<string>UIInterfaceOrientationLandscapeLeft</string>
34+
<string>UIInterfaceOrientationLandscapeRight</string>
4035
</array>
41-
<key>UIBackgroundModes</key>
36+
<key>UISupportedInterfaceOrientations~ipad</key>
4237
<array>
43-
<string>fetch</string>
38+
<string>UIInterfaceOrientationPortrait</string>
39+
<string>UIInterfaceOrientationPortraitUpsideDown</string>
40+
<string>UIInterfaceOrientationLandscapeLeft</string>
41+
<string>UIInterfaceOrientationLandscapeRight</string>
4442
</array>
43+
<key>NSLocalNetworkUsageDescription</key>
44+
<string>PlainApp needs local network access to discover and communicate with peer devices.</string>
45+
<key>NSPhotoLibraryUsageDescription</key>
46+
<string>PlainApp needs photo library access to browse and manage your photos.</string>
47+
<key>NSBluetoothAlwaysUsageDescription</key>
48+
<string>PlainApp needs Bluetooth to connect with nearby devices for peer-to-peer features.</string>
49+
<key>NSCameraUsageDescription</key>
50+
<string>PlainApp needs camera access for QR code scanning and document scanning.</string>
51+
<key>NSMicrophoneUsageDescription</key>
52+
<string>PlainApp needs microphone access for sound meter and audio recording features.</string>
53+
<key>NSContactsUsageDescription</key>
54+
<string>PlainApp needs contacts access to display and manage your contacts.</string>
4555
</dict>
4656
</plist>

iosApp/iosApp/PlainIOSApp.swift

Lines changed: 0 additions & 10 deletions
This file was deleted.

iosApp/iosApp/iosApp.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
import PlainShared
3+
4+
@main
5+
struct PlainApp: App {
6+
var body: some Scene {
7+
WindowGroup {
8+
ComposeView()
9+
.ignoresSafeArea()
10+
}
11+
}
12+
}
13+
14+
/// Wraps the Compose Multiplatform `MainViewController` in a SwiftUI view.
15+
struct ComposeView: UIViewControllerRepresentable {
16+
func makeUIViewController(context: Context) -> UIViewController {
17+
MainViewController()
18+
}
19+
20+
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
21+
}

shared/src/commonMain/kotlin/com/ismartcoding/plain/platform/MediaDurationFixQueue.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.ismartcoding.plain.events.MediaDurationZeroEvent
44
import com.ismartcoding.plain.events.MediaDurationZeroItem
55
import com.ismartcoding.plain.helpers.coIO
66
import kotlinx.coroutines.channels.Channel
7+
import kotlinx.coroutines.flow.MutableStateFlow
78
import kotlinx.coroutines.launch
89
import kotlinx.coroutines.sync.Mutex
910
import kotlinx.coroutines.sync.Semaphore
@@ -33,17 +34,17 @@ object MediaDurationFixQueue {
3334
private val mutex = Mutex()
3435
private val processing = mutableSetOf<String>()
3536

36-
@Volatile
37-
private var started = false
37+
// MutableStateFlow.compareAndSet provides the same atomic check-and-set
38+
// semantics as @Volatile + synchronized, but works on both JVM and Native.
39+
private val started = MutableStateFlow(false)
3840

3941
/**
4042
* Start the background worker. Safe to call multiple times — only the first
4143
* call has effect. The worker runs for the app lifetime; when the channel is
4244
* empty it simply suspends.
4345
*/
4446
fun start() {
45-
if (started) return
46-
started = true
47+
if (!started.compareAndSet(false, true)) return
4748
coIO {
4849
val semaphore = Semaphore(MAX_CONCURRENCY)
4950
while (true) {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.ismartcoding.plain
2+
3+
import androidx.compose.runtime.mutableStateOf
4+
import androidx.compose.runtime.remember
5+
import androidx.compose.ui.window.ComposeUIViewController
6+
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
7+
import androidx.lifecycle.viewmodel.compose.viewModel
8+
import androidx.navigation.NavHostController
9+
import androidx.room.RoomDatabase
10+
import androidx.sqlite.SQLiteConnection
11+
import com.ismartcoding.plain.db.DataInitializer
12+
import com.ismartcoding.plain.enums.DarkTheme
13+
import com.ismartcoding.plain.events.AppEvents
14+
import com.ismartcoding.plain.lib.logcat.DiskLogAdapter
15+
import com.ismartcoding.plain.lib.logcat.LogCat
16+
import com.ismartcoding.plain.platform.DiskLogFormatStrategy
17+
import com.ismartcoding.plain.platform.buildAppDatabase
18+
import com.ismartcoding.plain.platform.dataStoreFilePath
19+
import com.ismartcoding.plain.platform.initDatabase
20+
import com.ismartcoding.plain.preferences.LocalDarkTheme
21+
import com.ismartcoding.plain.preferences.SettingsProvider
22+
import com.ismartcoding.plain.preferences.initDataStore
23+
import com.ismartcoding.plain.ui.models.AudioPlaylistViewModel
24+
import com.ismartcoding.plain.ui.models.ChannelViewModel
25+
import com.ismartcoding.plain.ui.models.ChatViewModel
26+
import com.ismartcoding.plain.ui.models.MainViewModel
27+
import com.ismartcoding.plain.ui.models.PeerViewModel
28+
import com.ismartcoding.plain.ui.models.PomodoroViewModel
29+
import com.ismartcoding.plain.ui.page.Main
30+
import com.ismartcoding.plain.ui.theme.AppTheme
31+
import kotlinx.coroutines.MainScope
32+
import kotlinx.coroutines.launch
33+
import okio.Path.Companion.toPath
34+
import platform.Foundation.NSFileManager
35+
import platform.Foundation.NSSearchPathForDirectoriesInDomains
36+
import platform.Foundation.NSDocumentDirectory
37+
import platform.Foundation.NSUserDomainMask
38+
import platform.UIKit.UIViewController
39+
40+
private var initialized = false
41+
42+
/**
43+
* One-time iOS app initialization: DataStore, Room database, log adapters,
44+
* and event collectors. Idempotent — safe to call multiple times.
45+
*/
46+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
47+
fun initIosApp() {
48+
if (initialized) return
49+
initialized = true
50+
51+
// Ensure the datastore directory exists before OkioStorage tries to write
52+
val docsDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] as String
53+
val datastoreDir = "$docsDir/datastore"
54+
NSFileManager.defaultManager.createDirectoryAtPath(datastoreDir, withIntermediateDirectories = true, attributes = null, error = null)
55+
56+
// DataStore (multiplatform Preferences DataStore backed by okio)
57+
val dataStore = PreferenceDataStoreFactory.createWithPath(
58+
produceFile = { dataStoreFilePath().toPath() },
59+
)
60+
initDataStore(dataStore)
61+
62+
// Room database with the same onCreate seed data as Android
63+
initDatabase(
64+
buildAppDatabase(Constants.DATABASE_NAME)
65+
.addCallback(object : RoomDatabase.Callback() {
66+
override fun onCreate(connection: SQLiteConnection) {
67+
DataInitializer(connection).apply {
68+
insertWelcome()
69+
insertTags()
70+
insertNotes()
71+
}
72+
}
73+
})
74+
.build(),
75+
)
76+
77+
// Disk logging (isDebugBuild() returns false on iOS → WARN threshold)
78+
LogCat.addLogAdapter(
79+
DiskLogAdapter(
80+
DiskLogFormatStrategy.getInstance(),
81+
minPriority = LogCat.WARN,
82+
),
83+
)
84+
85+
// Global event collectors (MediaDurationFixQueue, sleep timer, etc.)
86+
AppEvents.register()
87+
}
88+
89+
/**
90+
* Entry point called from Swift. Returns a UIViewController hosting the
91+
* Compose Multiplatform UI tree (SettingsProvider → AppTheme → Main).
92+
*/
93+
@Suppress("unused")
94+
fun MainViewController(): UIViewController {
95+
initIosApp()
96+
return ComposeUIViewController {
97+
SettingsProvider {
98+
AppTheme(useDarkTheme = DarkTheme.isDarkTheme(LocalDarkTheme.current)) {
99+
val navControllerState = remember { mutableStateOf<NavHostController?>(null) }
100+
val mainVM: MainViewModel = viewModel()
101+
val audioPlaylistVM: AudioPlaylistViewModel = viewModel()
102+
val pomodoroVM: PomodoroViewModel = viewModel()
103+
val chatVM: ChatViewModel = viewModel()
104+
val peerVM: PeerViewModel = viewModel()
105+
val channelVM: ChannelViewModel = viewModel()
106+
Main(
107+
navControllerState = navControllerState,
108+
onLaunched = {
109+
// iOS has no Intent to handle; locale init runs in background.
110+
MainScope().launch { com.ismartcoding.plain.enums.Language.initLocaleAsync() }
111+
},
112+
mainVM = mainVM,
113+
audioPlaylistVM = audioPlaylistVM,
114+
pomodoroVM = pomodoroVM,
115+
chatVM = chatVM,
116+
peerVM = peerVM,
117+
channelVM = channelVM,
118+
)
119+
}
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)