Jelajahi Sumber

Initial commit

bob.yuxinyang 1 bulan lalu
melakukan
5e1f8130ee
45 mengubah file dengan 6730 tambahan dan 0 penghapusan
  1. 9 0
      .gitignore
  2. 749 0
      CelestiaTrace.xcodeproj/project.pbxproj
  3. 7 0
      CelestiaTrace.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  4. 25 0
      CelestiaTrace/App/CelestiaTraceApp.swift
  5. 51 0
      CelestiaTrace/App/ColorTheme.swift
  6. 23 0
      CelestiaTrace/Models/AudioChunk.swift
  7. 41 0
      CelestiaTrace/Models/BoundDevice.swift
  8. 56 0
      CelestiaTrace/Models/CelestiaSession.swift
  9. 44 0
      CelestiaTrace/Models/CelestiaTimelineEvent.swift
  10. 30 0
      CelestiaTrace/Models/UserAccount.swift
  11. 24 0
      CelestiaTrace/Services/Audio/AudioRecorderProtocol.swift
  12. 211 0
      CelestiaTrace/Services/Audio/RealAudioRecorder.swift
  13. 68 0
      CelestiaTrace/Services/Audio/RecordingEnvironmentDetector.swift
  14. 129 0
      CelestiaTrace/Services/Audio/SilenceDetector.swift
  15. 146 0
      CelestiaTrace/Services/Auth/AuthManager.swift
  16. 75 0
      CelestiaTrace/Services/Auth/AuthServiceProtocol.swift
  17. 203 0
      CelestiaTrace/Services/Auth/LocalAuthRepository.swift
  18. 47 0
      CelestiaTrace/Services/Auth/RemoteAuthServiceStub.swift
  19. 127 0
      CelestiaTrace/Services/Bluetooth/BLEManager.swift
  20. 37 0
      CelestiaTrace/Services/HapticManager.swift
  21. 62 0
      CelestiaTrace/Services/Network/MockNetworkService.swift
  22. 13 0
      CelestiaTrace/Services/Network/NetworkServiceProtocol.swift
  23. 369 0
      CelestiaTrace/ViewModels/PlaybackViewModel.swift
  24. 156 0
      CelestiaTrace/ViewModels/RecordingViewModel.swift
  25. 57 0
      CelestiaTrace/Views/Components/DiscreetWarningBanner.swift
  26. 45 0
      CelestiaTrace/Views/Components/ImagePicker.swift
  27. 79 0
      CelestiaTrace/Views/Components/PulsingRecordButton.swift
  28. 98 0
      CelestiaTrace/Views/Components/SyncAuthPromptModal.swift
  29. 78 0
      CelestiaTrace/Views/ContentView.swift
  30. 176 0
      CelestiaTrace/Views/Detail/ChronoFeedRow.swift
  31. 301 0
      CelestiaTrace/Views/Detail/MultiTrackTimeline.swift
  32. 399 0
      CelestiaTrace/Views/Detail/SessionDetailView.swift
  33. 237 0
      CelestiaTrace/Views/History/SessionListView.swift
  34. 303 0
      CelestiaTrace/Views/Home/HomeView.swift
  35. 226 0
      CelestiaTrace/Views/Profile/Auth/AuthModalView.swift
  36. 133 0
      CelestiaTrace/Views/Profile/Auth/ChangePasswordModalView.swift
  37. 134 0
      CelestiaTrace/Views/Profile/Auth/EditProfileModalView.swift
  38. 206 0
      CelestiaTrace/Views/Profile/Devices/DeviceListView.swift
  39. 203 0
      CelestiaTrace/Views/Profile/Devices/DeviceScanView.swift
  40. 411 0
      CelestiaTrace/Views/Profile/ProfileView.swift
  41. 429 0
      CelestiaTrace/Views/Recording/ActiveRecordingView.swift
  42. 119 0
      CelestiaTrace/Views/Recording/LiveWaveformView.swift
  43. 288 0
      CelestiaTrace/Views/Settings/SettingsView.swift
  44. 26 0
      Package.swift
  45. 80 0
      generate_project.rb

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+.DS_Store
+.build/
+.swiftpm/
+build_derived_data/
+DerivedData/
+*.xcuserstate
+xcuserdata/
+*.xccheckout
+*.xcscmblueprint

+ 749 - 0
CelestiaTrace.xcodeproj/project.pbxproj

@@ -0,0 +1,749 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		10EAADFFB07A1A8721B25D6B /* MockNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24923AF5D038F81EB3248A4C /* MockNetworkService.swift */; };
+		128B1C7150980D1FBE511B79 /* RecordingEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */; };
+		15EAFA0DBF80C789F02C5119 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086899076999CD4909BB9748 /* ProfileView.swift */; };
+		1A9161A601EB0954CD4B8A76 /* NetworkServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2D8AEE2E58FE0F2EF531D0 /* NetworkServiceProtocol.swift */; };
+		24472E311A9EB0785297B008 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1BE84E0C74D9F7BE9DC3E /* UserAccount.swift */; };
+		24D8D6477ED27DF7009D383C /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9AB65FF3C9678A6192C88 /* HapticManager.swift */; };
+		26D05B0A440FC9B26E89E4F0 /* AuthServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDFF4B7FD8488111C99DADF /* AuthServiceProtocol.swift */; };
+		2C16272F61F8B36A2EF69129 /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE663DCAAC5D1765BA31A3D /* SessionDetailView.swift */; };
+		2FD7EE603C849E281034CDD8 /* SyncAuthPromptModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C76F629F3460D6F077C3708 /* SyncAuthPromptModal.swift */; };
+		36E871DAD9477A21AE2F01B7 /* AuthModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10ED811D3048B961C4754A6 /* AuthModalView.swift */; };
+		3A7E453FDCA5DA9F96194B23 /* CelestiaTraceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628549E7FC16C58A15C9E1B7 /* CelestiaTraceApp.swift */; };
+		403219D9D8C972EC2AA8F84E /* EditProfileModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13A08133A50094CBFFDDF9 /* EditProfileModalView.swift */; };
+		44732C9690578EEFC6840D05 /* DeviceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CB67D1B2BDECAA772F3935 /* DeviceListView.swift */; };
+		490825791613AC52D06ECFA7 /* CelestiaSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CE4996BF0CC501E3B7C567 /* CelestiaSession.swift */; };
+		4F4476444CB6838B2BE6EE87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71787388F9456A4784534BCC /* ContentView.swift */; };
+		503E109CBBC69E3015AE37A3 /* SilenceDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEED3C75367F66D6D6CE745D /* SilenceDetector.swift */; };
+		58EFA65370C61ED9A8EE6627 /* ActiveRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B9693BD9924C08A1F2ECD1 /* ActiveRecordingView.swift */; };
+		70A52B64861B541B4C9423AC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5353BBE8E9552B4B486D8701 /* Foundation.framework */; };
+		75AE08DD135D81A3E19190EE /* DiscreetWarningBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */; };
+		7CA015A965BD30FAB3A92507 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073CAFDB8C45F4EAA33FA0EA /* AuthManager.swift */; };
+		8B5C51BB4474283BDF4ADACB /* LiveWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1189E47F735DDB40D1E741E6 /* LiveWaveformView.swift */; };
+		8FBEE86CDB9635710EF85F1B /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52FB139E193DBAC41F9D3F6 /* BLEManager.swift */; };
+		8FF835BDBD2A0E3551462510 /* LocalAuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8211C786643CA85F12CCAB4 /* LocalAuthRepository.swift */; };
+		936A52ADE1ECF30D3650B8D8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5353BBE8E9552B4B486D8701 /* Foundation.framework */; };
+		93F943133BCD792D991AA8E3 /* AudioChunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320789BA1CB176C4DBC51E99 /* AudioChunk.swift */; };
+		96791550D01A4FD10EEB9BA3 /* RecordingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64180D833523B84195EE7E2A /* RecordingViewModel.swift */; };
+		9BDA680497BF5DD72A7BA8F3 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6305E60488EEABAFE2E1D6D8 /* HomeView.swift */; };
+		9C5A0C9CDFCB8CFB10FF0B06 /* DeviceScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7063E6702335B02B65775F4 /* DeviceScanView.swift */; };
+		9E4554980E7FF484BF400458 /* MultiTrackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC9982D9E53E1A77CDB0470 /* MultiTrackTimeline.swift */; };
+		A3F43A5200A626CBFB70395E /* CelestiaTimelineEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DAA0FBAEC0BF91F45D15B1E /* CelestiaTimelineEvent.swift */; };
+		AF09E026C9AB15874D870E1F /* PulsingRecordButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */; };
+		B262A6AF17A835DB666B2FAB /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */; };
+		BE12EF8734ABF645B2B28BBB /* PlaybackViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E528009D607763F9E68EFBAD /* PlaybackViewModel.swift */; };
+		C2807F9F2794AFA8BF03F104 /* ChangePasswordModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BE0E22B2A472E2AFC9CB98 /* ChangePasswordModalView.swift */; };
+		C8794B9720CD85F518A82473 /* BoundDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */; };
+		CD45B55AB993D054E50A43BE /* RealAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977EB01CFA491A1573D7CF22 /* RealAudioRecorder.swift */; };
+		DDC0D9C4883B4861CBC74C39 /* ChronoFeedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */; };
+		E78FC729614A4C93C97D564D /* ColorTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */; };
+		F385E2FCB06D39917F51846E /* AudioRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B407473A307EEAF2CB5EE94 /* AudioRecorderProtocol.swift */; };
+		F3CB51CC262E0F4929EB129B /* RemoteAuthServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13AE9CB7E8C9E3BACFC679 /* RemoteAuthServiceStub.swift */; };
+		F5035B942DE1FEC10515D9D6 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */; };
+		FD30DBB57E19055AB8F0A956 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4883D42730E6133FCC3C81C2 /* SettingsView.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		55C3FF8383E750C9C9453EC5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 979128538DFF0B361F857241 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = EC79DCF0EBB2620B0FC21573;
+			remoteInfo = CelestiaTrace;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		073CAFDB8C45F4EAA33FA0EA /* AuthManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
+		086899076999CD4909BB9748 /* ProfileView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
+		0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SessionListView.swift; sourceTree = "<group>"; };
+		1189E47F735DDB40D1E741E6 /* LiveWaveformView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiveWaveformView.swift; sourceTree = "<group>"; };
+		1DC9982D9E53E1A77CDB0470 /* MultiTrackTimeline.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MultiTrackTimeline.swift; sourceTree = "<group>"; };
+		1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BoundDevice.swift; sourceTree = "<group>"; };
+		24923AF5D038F81EB3248A4C /* MockNetworkService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MockNetworkService.swift; sourceTree = "<group>"; };
+		320789BA1CB176C4DBC51E99 /* AudioChunk.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioChunk.swift; sourceTree = "<group>"; };
+		3C76F629F3460D6F077C3708 /* SyncAuthPromptModal.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SyncAuthPromptModal.swift; sourceTree = "<group>"; };
+		3FE663DCAAC5D1765BA31A3D /* SessionDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = "<group>"; };
+		4883D42730E6133FCC3C81C2 /* SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
+		4B407473A307EEAF2CB5EE94 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = "<group>"; };
+		4DAA0FBAEC0BF91F45D15B1E /* CelestiaTimelineEvent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaTimelineEvent.swift; sourceTree = "<group>"; };
+		4E13AE9CB7E8C9E3BACFC679 /* RemoteAuthServiceStub.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RemoteAuthServiceStub.swift; sourceTree = "<group>"; };
+		4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiscreetWarningBanner.swift; sourceTree = "<group>"; };
+		51A1BE84E0C74D9F7BE9DC3E /* UserAccount.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = "<group>"; };
+		5353BBE8E9552B4B486D8701 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
+		628549E7FC16C58A15C9E1B7 /* CelestiaTraceApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaTraceApp.swift; sourceTree = "<group>"; };
+		6305E60488EEABAFE2E1D6D8 /* HomeView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
+		64180D833523B84195EE7E2A /* RecordingViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingViewModel.swift; sourceTree = "<group>"; };
+		66BE0E22B2A472E2AFC9CB98 /* ChangePasswordModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangePasswordModalView.swift; sourceTree = "<group>"; };
+		71729F8F4A48D77C1898C27B /* CelestiaTraceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CelestiaTraceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		71787388F9456A4784534BCC /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PulsingRecordButton.swift; sourceTree = "<group>"; };
+		7F2D8AEE2E58FE0F2EF531D0 /* NetworkServiceProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkServiceProtocol.swift; sourceTree = "<group>"; };
+		83CE4996BF0CC501E3B7C567 /* CelestiaSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaSession.swift; sourceTree = "<group>"; };
+		8CDFF4B7FD8488111C99DADF /* AuthServiceProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthServiceProtocol.swift; sourceTree = "<group>"; };
+		9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChronoFeedRow.swift; sourceTree = "<group>"; };
+		95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ColorTheme.swift; sourceTree = "<group>"; };
+		977EB01CFA491A1573D7CF22 /* RealAudioRecorder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RealAudioRecorder.swift; sourceTree = "<group>"; };
+		AA13A08133A50094CBFFDDF9 /* EditProfileModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileModalView.swift; sourceTree = "<group>"; };
+		AEED3C75367F66D6D6CE745D /* SilenceDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SilenceDetector.swift; sourceTree = "<group>"; };
+		B7B9693BD9924C08A1F2ECD1 /* ActiveRecordingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActiveRecordingView.swift; sourceTree = "<group>"; };
+		B8211C786643CA85F12CCAB4 /* LocalAuthRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalAuthRepository.swift; sourceTree = "<group>"; };
+		DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
+		DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingEnvironmentDetector.swift; sourceTree = "<group>"; };
+		DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CelestiaTrace.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		E10ED811D3048B961C4754A6 /* AuthModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthModalView.swift; sourceTree = "<group>"; };
+		E528009D607763F9E68EFBAD /* PlaybackViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PlaybackViewModel.swift; sourceTree = "<group>"; };
+		E7063E6702335B02B65775F4 /* DeviceScanView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceScanView.swift; sourceTree = "<group>"; };
+		EAC9AB65FF3C9678A6192C88 /* HapticManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
+		F52FB139E193DBAC41F9D3F6 /* BLEManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
+		F8CB67D1B2BDECAA772F3935 /* DeviceListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceListView.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		41887A64604410C0C61AEBD5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				70A52B64861B541B4C9423AC /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		6FF5C5F71B01A15E70EBC20C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				936A52ADE1ECF30D3650B8D8 /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		21AD838392427D841273B592 /* Detail */ = {
+			isa = PBXGroup;
+			children = (
+				3FE663DCAAC5D1765BA31A3D /* SessionDetailView.swift */,
+				9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */,
+				1DC9982D9E53E1A77CDB0470 /* MultiTrackTimeline.swift */,
+			);
+			path = Detail;
+			sourceTree = "<group>";
+		};
+		2297351526BD0F24ED0714A0 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */,
+				71729F8F4A48D77C1898C27B /* CelestiaTraceTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		2626EC2897F60B86D833D875 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				83CE4996BF0CC501E3B7C567 /* CelestiaSession.swift */,
+				4DAA0FBAEC0BF91F45D15B1E /* CelestiaTimelineEvent.swift */,
+				320789BA1CB176C4DBC51E99 /* AudioChunk.swift */,
+				1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */,
+				51A1BE84E0C74D9F7BE9DC3E /* UserAccount.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
+		31A0E1032B1847F49A6241BD /* Home */ = {
+			isa = PBXGroup;
+			children = (
+				6305E60488EEABAFE2E1D6D8 /* HomeView.swift */,
+			);
+			path = Home;
+			sourceTree = "<group>";
+		};
+		32200F0540C3761CA9D1495A /* CelestiaTraceTests */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = CelestiaTraceTests;
+			path = Tests/CelestiaTraceTests;
+			sourceTree = "<group>";
+		};
+		44E01A3C9C72DBC8106DEE17 /* Profile */ = {
+			isa = PBXGroup;
+			children = (
+				086899076999CD4909BB9748 /* ProfileView.swift */,
+				7249FD4D94237968D3D5F3AC /* Auth */,
+				F7861FD2C1283A2FC2FDB934 /* Devices */,
+			);
+			path = Profile;
+			sourceTree = "<group>";
+		};
+		565448EB878982B445E267AF /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				AA9F20B44A68E171B39266B0 /* iOS */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		5BCD8C04CE34AA3B16B8C831 /* Auth */ = {
+			isa = PBXGroup;
+			children = (
+				B8211C786643CA85F12CCAB4 /* LocalAuthRepository.swift */,
+				8CDFF4B7FD8488111C99DADF /* AuthServiceProtocol.swift */,
+				4E13AE9CB7E8C9E3BACFC679 /* RemoteAuthServiceStub.swift */,
+				073CAFDB8C45F4EAA33FA0EA /* AuthManager.swift */,
+			);
+			path = Auth;
+			sourceTree = "<group>";
+		};
+		7249FD4D94237968D3D5F3AC /* Auth */ = {
+			isa = PBXGroup;
+			children = (
+				E10ED811D3048B961C4754A6 /* AuthModalView.swift */,
+				66BE0E22B2A472E2AFC9CB98 /* ChangePasswordModalView.swift */,
+				AA13A08133A50094CBFFDDF9 /* EditProfileModalView.swift */,
+			);
+			path = Auth;
+			sourceTree = "<group>";
+		};
+		7B90B8D292E18F60A02CDFE2 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				B7A7A2A0082F059CCD6BE791 /* Settings */,
+				31A0E1032B1847F49A6241BD /* Home */,
+				BC1D87D6D1785EA7D6AA6AF7 /* Recording */,
+				ADD73EAAF268C8B37E097585 /* Components */,
+				21AD838392427D841273B592 /* Detail */,
+				44E01A3C9C72DBC8106DEE17 /* Profile */,
+				96406E65C0CB98B68F7082F4 /* History */,
+				71787388F9456A4784534BCC /* ContentView.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		96406E65C0CB98B68F7082F4 /* History */ = {
+			isa = PBXGroup;
+			children = (
+				0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */,
+			);
+			path = History;
+			sourceTree = "<group>";
+		};
+		AA9F20B44A68E171B39266B0 /* iOS */ = {
+			isa = PBXGroup;
+			children = (
+				5353BBE8E9552B4B486D8701 /* Foundation.framework */,
+			);
+			name = iOS;
+			sourceTree = "<group>";
+		};
+		ADD73EAAF268C8B37E097585 /* Components */ = {
+			isa = PBXGroup;
+			children = (
+				4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */,
+				DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */,
+				3C76F629F3460D6F077C3708 /* SyncAuthPromptModal.swift */,
+				77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */,
+			);
+			path = Components;
+			sourceTree = "<group>";
+		};
+		AF823CBC596A095A54F096C1 /* Bluetooth */ = {
+			isa = PBXGroup;
+			children = (
+				F52FB139E193DBAC41F9D3F6 /* BLEManager.swift */,
+			);
+			path = Bluetooth;
+			sourceTree = "<group>";
+		};
+		B33580BE3AF0DC9085B1D3A1 /* Services */ = {
+			isa = PBXGroup;
+			children = (
+				5BCD8C04CE34AA3B16B8C831 /* Auth */,
+				EAC9AB65FF3C9678A6192C88 /* HapticManager.swift */,
+				DB59940538488B1E753621A8 /* Network */,
+				FD4D76A3358AAF1519C279F1 /* Audio */,
+				AF823CBC596A095A54F096C1 /* Bluetooth */,
+			);
+			path = Services;
+			sourceTree = "<group>";
+		};
+		B5CB52A86415BBB0B9DCE932 /* CelestiaTrace */ = {
+			isa = PBXGroup;
+			children = (
+				CC066F5A1023EDB077FDD88E /* ViewModels */,
+				DA204128E169127A0B8C4B30 /* App */,
+				2626EC2897F60B86D833D875 /* Models */,
+				7B90B8D292E18F60A02CDFE2 /* Views */,
+				B33580BE3AF0DC9085B1D3A1 /* Services */,
+			);
+			path = CelestiaTrace;
+			sourceTree = "<group>";
+		};
+		B7A7A2A0082F059CCD6BE791 /* Settings */ = {
+			isa = PBXGroup;
+			children = (
+				4883D42730E6133FCC3C81C2 /* SettingsView.swift */,
+			);
+			path = Settings;
+			sourceTree = "<group>";
+		};
+		BC1D87D6D1785EA7D6AA6AF7 /* Recording */ = {
+			isa = PBXGroup;
+			children = (
+				B7B9693BD9924C08A1F2ECD1 /* ActiveRecordingView.swift */,
+				1189E47F735DDB40D1E741E6 /* LiveWaveformView.swift */,
+			);
+			path = Recording;
+			sourceTree = "<group>";
+		};
+		CC066F5A1023EDB077FDD88E /* ViewModels */ = {
+			isa = PBXGroup;
+			children = (
+				E528009D607763F9E68EFBAD /* PlaybackViewModel.swift */,
+				64180D833523B84195EE7E2A /* RecordingViewModel.swift */,
+			);
+			path = ViewModels;
+			sourceTree = "<group>";
+		};
+		D9CB1BAF8E8DB5E4E073CC77 = {
+			isa = PBXGroup;
+			children = (
+				2297351526BD0F24ED0714A0 /* Products */,
+				565448EB878982B445E267AF /* Frameworks */,
+				B5CB52A86415BBB0B9DCE932 /* CelestiaTrace */,
+				32200F0540C3761CA9D1495A /* CelestiaTraceTests */,
+			);
+			sourceTree = "<group>";
+		};
+		DA204128E169127A0B8C4B30 /* App */ = {
+			isa = PBXGroup;
+			children = (
+				95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */,
+				628549E7FC16C58A15C9E1B7 /* CelestiaTraceApp.swift */,
+			);
+			path = App;
+			sourceTree = "<group>";
+		};
+		DB59940538488B1E753621A8 /* Network */ = {
+			isa = PBXGroup;
+			children = (
+				7F2D8AEE2E58FE0F2EF531D0 /* NetworkServiceProtocol.swift */,
+				24923AF5D038F81EB3248A4C /* MockNetworkService.swift */,
+			);
+			path = Network;
+			sourceTree = "<group>";
+		};
+		F7861FD2C1283A2FC2FDB934 /* Devices */ = {
+			isa = PBXGroup;
+			children = (
+				F8CB67D1B2BDECAA772F3935 /* DeviceListView.swift */,
+				E7063E6702335B02B65775F4 /* DeviceScanView.swift */,
+			);
+			path = Devices;
+			sourceTree = "<group>";
+		};
+		FD4D76A3358AAF1519C279F1 /* Audio */ = {
+			isa = PBXGroup;
+			children = (
+				AEED3C75367F66D6D6CE745D /* SilenceDetector.swift */,
+				DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */,
+				977EB01CFA491A1573D7CF22 /* RealAudioRecorder.swift */,
+				4B407473A307EEAF2CB5EE94 /* AudioRecorderProtocol.swift */,
+			);
+			path = Audio;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		D5681575EDB1ABEE4093C509 /* CelestiaTraceTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 0E7E08AA696927CB2EBFCE00 /* Build configuration list for PBXNativeTarget "CelestiaTraceTests" */;
+			buildPhases = (
+				1F77E010196A2F5F89E27F19 /* Sources */,
+				41887A64604410C0C61AEBD5 /* Frameworks */,
+				D875758EF038ED00E0A14D96 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				6F0B632A5E411D7F9FFE0767 /* PBXTargetDependency */,
+			);
+			name = CelestiaTraceTests;
+			productName = CelestiaTraceTests;
+			productReference = 71729F8F4A48D77C1898C27B /* CelestiaTraceTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		EC79DCF0EBB2620B0FC21573 /* CelestiaTrace */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 902A4F79574BE80E9469463E /* Build configuration list for PBXNativeTarget "CelestiaTrace" */;
+			buildPhases = (
+				9E60C6991F09AB5E80EF4283 /* Sources */,
+				6FF5C5F71B01A15E70EBC20C /* Frameworks */,
+				2B844E255303FD43D7D16A02 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = CelestiaTrace;
+			productName = CelestiaTrace;
+			productReference = DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		979128538DFF0B361F857241 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 1600;
+				LastUpgradeCheck = 1600;
+				TargetAttributes = {
+					EC79DCF0EBB2620B0FC21573 = {
+						DevelopmentTeam = 5ERVQEJ497;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = C378CBFE73C70F1388740C61 /* Build configuration list for PBXProject "CelestiaTrace" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = D9CB1BAF8E8DB5E4E073CC77;
+			productRefGroup = 2297351526BD0F24ED0714A0 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				EC79DCF0EBB2620B0FC21573 /* CelestiaTrace */,
+				D5681575EDB1ABEE4093C509 /* CelestiaTraceTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		2B844E255303FD43D7D16A02 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		D875758EF038ED00E0A14D96 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		1F77E010196A2F5F89E27F19 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		9E60C6991F09AB5E80EF4283 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				BE12EF8734ABF645B2B28BBB /* PlaybackViewModel.swift in Sources */,
+				96791550D01A4FD10EEB9BA3 /* RecordingViewModel.swift in Sources */,
+				E78FC729614A4C93C97D564D /* ColorTheme.swift in Sources */,
+				3A7E453FDCA5DA9F96194B23 /* CelestiaTraceApp.swift in Sources */,
+				490825791613AC52D06ECFA7 /* CelestiaSession.swift in Sources */,
+				A3F43A5200A626CBFB70395E /* CelestiaTimelineEvent.swift in Sources */,
+				93F943133BCD792D991AA8E3 /* AudioChunk.swift in Sources */,
+				C8794B9720CD85F518A82473 /* BoundDevice.swift in Sources */,
+				24472E311A9EB0785297B008 /* UserAccount.swift in Sources */,
+				FD30DBB57E19055AB8F0A956 /* SettingsView.swift in Sources */,
+				9BDA680497BF5DD72A7BA8F3 /* HomeView.swift in Sources */,
+				58EFA65370C61ED9A8EE6627 /* ActiveRecordingView.swift in Sources */,
+				8B5C51BB4474283BDF4ADACB /* LiveWaveformView.swift in Sources */,
+				75AE08DD135D81A3E19190EE /* DiscreetWarningBanner.swift in Sources */,
+				F5035B942DE1FEC10515D9D6 /* ImagePicker.swift in Sources */,
+				2FD7EE603C849E281034CDD8 /* SyncAuthPromptModal.swift in Sources */,
+				AF09E026C9AB15874D870E1F /* PulsingRecordButton.swift in Sources */,
+				2C16272F61F8B36A2EF69129 /* SessionDetailView.swift in Sources */,
+				DDC0D9C4883B4861CBC74C39 /* ChronoFeedRow.swift in Sources */,
+				9E4554980E7FF484BF400458 /* MultiTrackTimeline.swift in Sources */,
+				15EAFA0DBF80C789F02C5119 /* ProfileView.swift in Sources */,
+				36E871DAD9477A21AE2F01B7 /* AuthModalView.swift in Sources */,
+				C2807F9F2794AFA8BF03F104 /* ChangePasswordModalView.swift in Sources */,
+				403219D9D8C972EC2AA8F84E /* EditProfileModalView.swift in Sources */,
+				44732C9690578EEFC6840D05 /* DeviceListView.swift in Sources */,
+				9C5A0C9CDFCB8CFB10FF0B06 /* DeviceScanView.swift in Sources */,
+				B262A6AF17A835DB666B2FAB /* SessionListView.swift in Sources */,
+				4F4476444CB6838B2BE6EE87 /* ContentView.swift in Sources */,
+				8FF835BDBD2A0E3551462510 /* LocalAuthRepository.swift in Sources */,
+				26D05B0A440FC9B26E89E4F0 /* AuthServiceProtocol.swift in Sources */,
+				F3CB51CC262E0F4929EB129B /* RemoteAuthServiceStub.swift in Sources */,
+				7CA015A965BD30FAB3A92507 /* AuthManager.swift in Sources */,
+				24D8D6477ED27DF7009D383C /* HapticManager.swift in Sources */,
+				1A9161A601EB0954CD4B8A76 /* NetworkServiceProtocol.swift in Sources */,
+				10EAADFFB07A1A8721B25D6B /* MockNetworkService.swift in Sources */,
+				503E109CBBC69E3015AE37A3 /* SilenceDetector.swift in Sources */,
+				128B1C7150980D1FBE511B79 /* RecordingEnvironmentDetector.swift in Sources */,
+				CD45B55AB993D054E50A43BE /* RealAudioRecorder.swift in Sources */,
+				F385E2FCB06D39917F51846E /* AudioRecorderProtocol.swift in Sources */,
+				8FBEE86CDB9635710EF85F1B /* BLEManager.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		6F0B632A5E411D7F9FFE0767 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = CelestiaTrace;
+			target = EC79DCF0EBB2620B0FC21573 /* CelestiaTrace */;
+			targetProxy = 55C3FF8383E750C9C9453EC5 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		76F4E0F0A78EC9D84B954B50 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				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;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		95AB591F506D02959044D749 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_ASSET_PATHS = "\"\"";
+				DEVELOPMENT_TEAM = 5ERVQEJ497;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "需要蓝牙权限以扫描和绑定 BLE 录音设备";
+				INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "需要蓝牙权限以与录音设备传输数据";
+				INFOPLIST_KEY_NSCameraUsageDescription = "需要相机权限以在现场录制中拍摄照片记录";
+				INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要麦克风权限以进行会议现场的音轨录音";
+				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要相册权限以选择照片记录";
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.celestia.trace;
+				SDKROOT = iphoneos;
+				SWIFT_VERSION = 5.9;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Debug;
+		};
+		B1CCB86EBE298ADE0B572050 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_ASSET_PATHS = "\"\"";
+				DEVELOPMENT_TEAM = 5ERVQEJ497;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "需要蓝牙权限以扫描和绑定 BLE 录音设备";
+				INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "需要蓝牙权限以与录音设备传输数据";
+				INFOPLIST_KEY_NSCameraUsageDescription = "需要相机权限以在现场录制中拍摄照片记录";
+				INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要麦克风权限以进行会议现场的音轨录音";
+				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要相册权限以选择照片记录";
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.celestia.trace;
+				SDKROOT = iphoneos;
+				SWIFT_VERSION = 5.9;
+				TARGETED_DEVICE_FAMILY = 1;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		B827886CD5B378B8C90638F7 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				SDKROOT = iphoneos;
+				SWIFT_VERSION = 5.9;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CelestiaTrace.app/CelestiaTrace";
+				TEST_TARGET_NAME = CelestiaTrace;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		BCD7DB2413FD475C8DCF0B12 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				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;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		D16E5B27804289B138250469 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				SDKROOT = iphoneos;
+				SWIFT_VERSION = 5.9;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CelestiaTrace.app/CelestiaTrace";
+				TEST_TARGET_NAME = CelestiaTrace;
+			};
+			name = Debug;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		0E7E08AA696927CB2EBFCE00 /* Build configuration list for PBXNativeTarget "CelestiaTraceTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				B827886CD5B378B8C90638F7 /* Release */,
+				D16E5B27804289B138250469 /* Debug */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		902A4F79574BE80E9469463E /* Build configuration list for PBXNativeTarget "CelestiaTrace" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				B1CCB86EBE298ADE0B572050 /* Release */,
+				95AB591F506D02959044D749 /* Debug */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		C378CBFE73C70F1388740C61 /* Build configuration list for PBXProject "CelestiaTrace" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				76F4E0F0A78EC9D84B954B50 /* Debug */,
+				BCD7DB2413FD475C8DCF0B12 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 979128538DFF0B361F857241 /* Project object */;
+}

+ 7 - 0
CelestiaTrace.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>

+ 25 - 0
CelestiaTrace/App/CelestiaTraceApp.swift

@@ -0,0 +1,25 @@
+import SwiftUI
+import SwiftData
+
+@main
+struct CelestiaTraceApp: App {
+    let container: ModelContainer
+    
+    init() {
+        do {
+            let schema = Schema([CelestiaSession.self, CelestiaTimelineEvent.self, AudioChunk.self])
+            let config = ModelConfiguration(isStoredInMemoryOnly: false)
+            let localContainer = try ModelContainer(for: schema, configurations: config)
+            self.container = localContainer
+        } catch {
+            fatalError("Failed to initialize SwiftData Container: \(error)")
+        }
+    }
+    
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+        }
+        .modelContainer(container)
+    }
+}

+ 51 - 0
CelestiaTrace/App/ColorTheme.swift

@@ -0,0 +1,51 @@
+import SwiftUI
+
+extension Color {
+    /// Premium business accent (refined slate/navy blue, or dynamic primary)
+    static let celestiaCyan = Color.primary
+    
+    /// Dynamic main background (Pure white in Light Mode, obsidian black in Dark Mode)
+    static let spaceBlack = Color(uiColor: .systemBackground)
+    
+    /// Refined secondary outline/accent (monochrome slate grey)
+    static let nebulaPurple = Color.secondary
+    
+    /// High-contrast primary text
+    static let starWhite = Color.primary
+    
+    /// Dynamic cards background (subtle light grey in Light Mode, deep dark grey in Dark Mode)
+    static let cardBackground = Color(uiColor: .secondarySystemBackground)
+    
+    /// Monospace-style subtle metadata grey
+    static let subtleGray = Color.secondary
+    
+    /// The standardized line border color for wireframes (thin contrast lines)
+    static let lineBorder = Color.primary.opacity(0.12)
+    
+    /// Precise red indicator for recording
+    static let recordingRed = Color(red: 229/255, green: 57/255, blue: 53/255)
+}
+
+/// A no-op glow that replaces NeonGlow to avoid compile errors while removing glows.
+struct NeonGlow: ViewModifier {
+    var color: Color = .clear
+    var radius: CGFloat = 0
+    
+    func body(content: Content) -> some View {
+        content
+    }
+}
+
+extension View {
+    func neonGlow(color: Color = .clear, radius: CGFloat = 0) -> some View {
+        modifier(NeonGlow(color: color, radius: radius))
+    }
+    
+    /// Applies a thin, sharp outline suitable for minimalist business interfaces.
+    func businessBorder(cornerRadius: CGFloat = 8) -> some View {
+        self.overlay(
+            RoundedRectangle(cornerRadius: cornerRadius)
+                .stroke(Color.lineBorder, lineWidth: 1)
+        )
+    }
+}

+ 23 - 0
CelestiaTrace/Models/AudioChunk.swift

@@ -0,0 +1,23 @@
+import Foundation
+import SwiftData
+
+@Model
+final class AudioChunk {
+    var id: UUID
+    var chunkIndex: Int
+    var localFilePath: String
+    var durationMs: Int64
+    var offsetInSessionMs: Int64
+    var createdAt: Date
+    
+    var session: CelestiaSession?
+    
+    init(chunkIndex: Int, localFilePath: String, durationMs: Int64, offsetInSessionMs: Int64) {
+        self.id = UUID()
+        self.chunkIndex = chunkIndex
+        self.localFilePath = localFilePath
+        self.durationMs = durationMs
+        self.offsetInSessionMs = offsetInSessionMs
+        self.createdAt = Date()
+    }
+}

+ 41 - 0
CelestiaTrace/Models/BoundDevice.swift

@@ -0,0 +1,41 @@
+import Foundation
+
+/// Represents a physical recording device bound to a user account via BLE.
+struct BoundDevice: Codable, Identifiable, Equatable {
+    let id: String           // Unique device hardware ID / MAC / Serial
+    var name: String         // Display name of the device
+    var peripheralUUID: String // CoreBluetooth Peripheral UUID
+    var boundAt: Date        // Date when bound to user account
+    var isConnected: Bool    // Connection status (runtime state)
+    var batteryLevel: Int?   // Battery percentage (0 - 100)
+    var firmwareVersion: String? // Firmware version
+    var boundUserId: String  // User account ID owning this device
+    
+    init(
+        id: String = UUID().uuidString,
+        name: String,
+        peripheralUUID: String = UUID().uuidString,
+        boundAt: Date = Date(),
+        isConnected: Bool = false,
+        batteryLevel: Int? = 85,
+        firmwareVersion: String? = "v1.2.0",
+        boundUserId: String
+    ) {
+        self.id = id
+        self.name = name
+        self.peripheralUUID = peripheralUUID
+        self.boundAt = boundAt
+        self.isConnected = isConnected
+        self.batteryLevel = batteryLevel
+        self.firmwareVersion = firmwareVersion
+        self.boundUserId = boundUserId
+    }
+}
+
+/// Helper structure for BLE scanning discovery results
+struct DiscoveredBLEDevice: Identifiable, Equatable {
+    let id: String
+    let name: String
+    let rssi: Int
+    let peripheralUUID: String
+}

+ 56 - 0
CelestiaTrace/Models/CelestiaSession.swift

@@ -0,0 +1,56 @@
+import Foundation
+import SwiftData
+
+@Model
+final class CelestiaSession {
+    var id: UUID
+    var title: String
+    var startTime: Date
+    var endTime: Date?
+    var localAudioPath: String?
+    var isSynced: Bool
+    var cloudSessionId: String?
+    
+    @Relationship(deleteRule: .cascade, inverse: \CelestiaTimelineEvent.session)
+    var events: [CelestiaTimelineEvent]
+    
+    @Relationship(deleteRule: .cascade, inverse: \AudioChunk.session)
+    var audioChunks: [AudioChunk]
+    
+    var durationMs: Int64 {
+        guard let end = endTime else { return 0 }
+        return Int64(end.timeIntervalSince(startTime) * 1000)
+    }
+    
+    var durationFormatted: String {
+        guard let end = endTime else { return "正在录音..." }
+        let interval = end.timeIntervalSince(startTime)
+        let hours = Int(interval) / 3600
+        let minutes = (Int(interval) % 3600) / 60
+        let seconds = Int(interval) % 60
+        if hours > 0 {
+            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
+        }
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+    
+    var photoCount: Int {
+        events.filter { $0.eventType == "PHOTO" }.count
+    }
+    
+    var noteCount: Int {
+        events.filter { $0.eventType == "NOTE" || $0.eventType == "MARKER" }.count
+    }
+    
+    init(title: String, localAudioPath: String? = nil) {
+        self.id = UUID()
+        self.title = title
+        self.startTime = Date()
+        self.endTime = nil
+        self.localAudioPath = localAudioPath
+        self.isSynced = false
+        self.cloudSessionId = nil
+        self.events = []
+        self.audioChunks = []
+    }
+}

+ 44 - 0
CelestiaTrace/Models/CelestiaTimelineEvent.swift

@@ -0,0 +1,44 @@
+import Foundation
+import SwiftData
+
+@Model
+final class CelestiaTimelineEvent {
+    var id: UUID
+    var relativeTimeMs: Int64
+    var eventType: String  // NOTE, PHOTO, MARKER, VOICE
+    var textContent: String?
+    var localFilePath: String?
+    var voiceStartOffsetMs: Int64?
+    var voiceEndOffsetMs: Int64?
+    var createdAt: Date
+    
+    var session: CelestiaSession?
+    
+    var relativeTimeFormatted: String {
+        let totalSeconds = Int(relativeTimeMs / 1000)
+        let hours = totalSeconds / 3600
+        let minutes = (totalSeconds % 3600) / 60
+        let seconds = totalSeconds % 60
+        if hours > 0 {
+            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
+        }
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+    
+    var eventIcon: String {
+        switch eventType {
+        case "PHOTO": return "camera.fill"
+        case "NOTE": return "note.text"
+        case "MARKER": return "exclamationmark.triangle.fill"
+        case "VOICE": return "mic.fill"
+        default: return "circle.fill"
+        }
+    }
+    
+    init(relativeTimeMs: Int64, eventType: String) {
+        self.id = UUID()
+        self.relativeTimeMs = relativeTimeMs
+        self.eventType = eventType
+        self.createdAt = Date()
+    }
+}

+ 30 - 0
CelestiaTrace/Models/UserAccount.swift

@@ -0,0 +1,30 @@
+import Foundation
+
+/// Data model representing a registered user account in CelestiaTrace.
+struct UserAccount: Codable, Identifiable, Equatable {
+    let id: String
+    var username: String
+    var email: String?
+    var phoneNumber: String?
+    var avatarURL: String?
+    var token: String
+    var registeredAt: Date
+    
+    init(
+        id: String = UUID().uuidString,
+        username: String,
+        email: String? = nil,
+        phoneNumber: String? = nil,
+        avatarURL: String? = nil,
+        token: String = UUID().uuidString,
+        registeredAt: Date = Date()
+    ) {
+        self.id = id
+        self.username = username
+        self.email = email
+        self.phoneNumber = phoneNumber
+        self.avatarURL = avatarURL
+        self.token = token
+        self.registeredAt = registeredAt
+    }
+}

+ 24 - 0
CelestiaTrace/Services/Audio/AudioRecorderProtocol.swift

@@ -0,0 +1,24 @@
+import Foundation
+import Combine
+
+/// Protocol abstracting audio recording capabilities.
+/// Allows swapping between Mock and Real (AVAudioEngine) implementations.
+protocol AudioRecorderProtocol: ObservableObject {
+    /// Current recording state
+    var isRecording: Bool { get }
+    /// Elapsed recording time in seconds
+    var elapsedTime: TimeInterval { get }
+    /// Current amplitude level (0.0 - 1.0) for waveform visualization
+    var currentAmplitude: Float { get }
+    /// Rolling buffer of recent amplitude samples for waveform drawing
+    var waveformSamples: [Float] { get }
+    /// The local file URL of the current/last recording. nil for mock.
+    var outputFileURL: URL? { get }
+    /// Whether the active recording is paused
+    var isPaused: Bool { get }
+    
+    func startRecording()
+    func stopRecording()
+    func pauseRecording()
+    func resumeRecording()
+}

+ 211 - 0
CelestiaTrace/Services/Audio/RealAudioRecorder.swift

@@ -0,0 +1,211 @@
+import Foundation
+import Combine
+import AVFoundation
+
+/// Real audio recorder using AVAudioRecorder for production recording.
+/// Requests microphone permissions, configures AVAudioSession, and outputs AAC (.m4a) files.
+final class RealAudioRecorder: AudioRecorderProtocol {
+    // MARK: - Published Properties
+    
+    @Published var isRecording: Bool = false
+    @Published var elapsedTime: TimeInterval = 0
+    @Published var currentAmplitude: Float = 0
+    @Published var waveformSamples: [Float] = []
+    @Published var outputFileURL: URL? = nil
+    @Published var isPaused: Bool = false
+    
+    // MARK: - Private Properties
+    
+    private var audioRecorder: AVAudioRecorder?
+    private var timer: Timer?
+    private let timerInterval: TimeInterval = 0.05
+    private let maxSampleCount = 200
+    
+    init() {
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleInterruption),
+            name: AVAudioSession.interruptionNotification,
+            object: AVAudioSession.sharedInstance()
+        )
+    }
+    
+    // MARK: - AudioRecorderProtocol
+    
+    func startRecording() {
+        guard !isRecording else { return }
+        
+        // Reset state
+        elapsedTime = 0
+        currentAmplitude = 0
+        waveformSamples = []
+        outputFileURL = nil
+        
+        // Request Permission
+        let session = AVAudioSession.sharedInstance()
+        session.requestRecordPermission { [weak self] granted in
+            guard let self else { return }
+            if granted {
+                DispatchQueue.main.async {
+                    self.setupAndRecord()
+                }
+            } else {
+                print("[RealAudioRecorder] Microphone permission denied")
+            }
+        }
+    }
+    
+    private func setupAndRecord() {
+        let session = AVAudioSession.sharedInstance()
+        do {
+            // Set up audio session category and mode for recording, ducking other apps
+            try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth, .duckOthers])
+            try session.setActive(true)
+            
+            // Create target file URL
+            let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
+            let filename = "recording_\(UUID().uuidString).m4a"
+            let fileURL = documents.appendingPathComponent(filename)
+            self.outputFileURL = fileURL
+            
+            // Configure AAC recorder settings
+            let settings: [String: Any] = [
+                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+                AVSampleRateKey: 12000.0,
+                AVNumberOfChannelsKey: 1,
+                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
+            ]
+            
+            let recorder = try AVAudioRecorder(url: fileURL, settings: settings)
+            recorder.isMeteringEnabled = true
+            
+            if recorder.prepareToRecord() {
+                recorder.record()
+                self.audioRecorder = recorder
+                self.isRecording = true
+                self.isPaused = false
+                self.startTimer()
+                print("[RealAudioRecorder] Recording started: \(fileURL.lastPathComponent)")
+            } else {
+                print("[RealAudioRecorder] Prepare to record failed")
+            }
+        } catch {
+            print("[RealAudioRecorder] Setup failed: \(error.localizedDescription)")
+        }
+    }
+    
+    func stopRecording() {
+        guard isRecording else { return }
+        
+        invalidateTimer()
+        audioRecorder?.stop()
+        audioRecorder = nil
+        isRecording = false
+        isPaused = false
+        currentAmplitude = 0
+        
+        // Deactivate audio session
+        try? AVAudioSession.sharedInstance().setActive(false)
+        print("[RealAudioRecorder] Recording stopped. File saved.")
+    }
+    
+    func pauseRecording() {
+        guard isRecording else { return }
+        audioRecorder?.pause()
+        isPaused = true
+        invalidateTimer()
+    }
+    
+    func resumeRecording() {
+        guard isRecording else { return }
+        try? AVAudioSession.sharedInstance().setActive(true)
+        audioRecorder?.record()
+        isPaused = false
+        startTimer()
+    }
+    
+    // MARK: - Private Helpers
+    
+    private func startTimer() {
+        invalidateTimer()
+        timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { [weak self] _ in
+            self?.tick()
+        }
+        if let timer {
+            RunLoop.current.add(timer, forMode: .common)
+        }
+    }
+    
+    private func invalidateTimer() {
+        timer?.invalidate()
+        timer = nil
+    }
+    
+    private func tick() {
+        guard let recorder = audioRecorder, recorder.isRecording else { return }
+        
+        // Update metering
+        recorder.updateMeters()
+        elapsedTime = recorder.currentTime
+        
+        // Get average decibels for channel 0
+        let power = recorder.averagePower(forChannel: 0)
+        
+        // Map decibels (-50.0 dB to 0.0 dB) to 0.0 - 1.0 amplitude
+        let minDb: Float = -50.0
+        let level: Float
+        if power < minDb {
+            level = 0
+        } else if power >= 0 {
+            level = 1
+        } else {
+            level = (power - minDb) / -minDb
+        }
+        
+        currentAmplitude = level
+        waveformSamples.append(level)
+        
+        if waveformSamples.count > maxSampleCount {
+            waveformSamples.removeFirst(waveformSamples.count - maxSampleCount)
+        }
+    }
+    
+    @objc private func handleInterruption(notification: Notification) {
+        guard let userInfo = notification.userInfo,
+              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
+              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
+            return
+        }
+        
+        switch type {
+        case .began:
+            DispatchQueue.main.async { [weak self] in
+                guard let self else { return }
+                if self.isRecording && !self.isPaused {
+                    self.pauseRecording()
+                    print("[RealAudioRecorder] Audio session interrupted. Recording automatically paused.")
+                }
+            }
+        case .ended:
+            guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
+            let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
+            if options.contains(.shouldResume) {
+                DispatchQueue.main.async { [weak self] in
+                    guard let self else { return }
+                    if self.isRecording && self.isPaused {
+                        self.resumeRecording()
+                        print("[RealAudioRecorder] Interruption ended. Recording automatically resumed.")
+                    }
+                }
+            }
+        @unknown default:
+            break
+        }
+    }
+    
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+        invalidateTimer()
+        audioRecorder?.stop()
+    }
+}

+ 68 - 0
CelestiaTrace/Services/Audio/RecordingEnvironmentDetector.swift

@@ -0,0 +1,68 @@
+import Foundation
+import AVFoundation
+import Combine
+
+/// Detects system environment issues before starting a recording.
+/// Checks background audio, media volume, and microphone permissions.
+@Observable
+final class RecordingEnvironmentDetector {
+    
+    // MARK: - Published Warnings
+    
+    var isOtherAudioPlaying: Bool = false
+    var isVolumeTooHigh: Bool = false
+    var micPermissionStatus: AVAudioSession.RecordPermission = .undetermined
+    
+    /// List of human-readable warnings based on current environment state.
+    var activeWarnings: [String] = []
+    
+    init() {
+        checkEnvironment()
+        
+        // Listen to volume changes or other audio notifications if necessary
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleAudioRouteChanged),
+            name: AVAudioSession.routeChangeNotification,
+            object: nil
+        )
+    }
+    
+    /// Runs environment checks and updates warnings.
+    func checkEnvironment() {
+        let session = AVAudioSession.sharedInstance()
+        
+        // 1. Check if other audio is playing in the background
+        self.isOtherAudioPlaying = session.isOtherAudioPlaying
+        
+        // 2. Check if output volume is too high (above 70%)
+        self.isVolumeTooHigh = session.outputVolume > 0.7
+        
+        // 3. Check microphone permission status
+        self.micPermissionStatus = session.recordPermission
+        
+        // Compile warning strings
+        var warnings: [String] = []
+        
+        if isOtherAudioPlaying {
+            warnings.append("⚠️ 检测到后台有其他音频播放中,建议关闭以防混入录音。")
+        }
+        
+        if isVolumeTooHigh {
+            warnings.append("⚠️ 系统媒体音量过高 (%.0f%%),录音时建议降低音量。" .replacingOccurrences(of: "%.0f%%", with: String(format: "%.0f%%", session.outputVolume * 100)))
+        }
+        
+        if micPermissionStatus == .denied {
+            warnings.append("⚠️ 麦克风访问权限被禁用,请前往系统设置开启麦克风。")
+        }
+        
+        self.activeWarnings = warnings
+    }
+    
+    @objc private func handleAudioRouteChanged() {
+        // Run checks on background thread to avoid blocking main loop, then dispatch updates
+        DispatchQueue.main.async { [weak self] in
+            self?.checkEnvironment()
+        }
+    }
+}

+ 129 - 0
CelestiaTrace/Services/Audio/SilenceDetector.swift

@@ -0,0 +1,129 @@
+import Foundation
+import AVFoundation
+
+/// Represents a detected silent interval within an audio recording.
+public struct SilenceRange: Codable, Hashable, Sendable {
+    public let start: TimeInterval // in seconds
+    public let end: TimeInterval   // in seconds
+    
+    public var duration: TimeInterval {
+        end - start
+    }
+    
+    public init(start: TimeInterval, end: TimeInterval) {
+        self.start = start
+        self.end = end
+    }
+}
+
+public final class SilenceDetector: Sendable {
+    /// Detects silent ranges in the specified audio file.
+    /// - Parameters:
+    ///   - audioURL: Local file URL of the audio.
+    ///   - thresholdDB: Threshold in decibels (e.g. -40.0 dB). Sounds below this are considered silent.
+    ///   - minDuration: Minimum consecutive duration in seconds to qualify as a silent segment.
+    /// - Returns: An array of detected SilenceRange objects.
+    public static func detectSilence(
+        in audioURL: URL,
+        thresholdDB: Float = -40.0,
+        minDuration: TimeInterval = 2.0
+    ) async -> [SilenceRange] {
+        let fm = FileManager.default
+        // Fallback to mock data if the file doesn't exist locally (for simulator and mock prototyping)
+        guard !audioURL.path.isEmpty && fm.fileExists(atPath: audioURL.path) else {
+            print("[SilenceDetector] File not found or empty path: \(audioURL.path). Returning mock silence ranges.")
+            return [
+                SilenceRange(start: 5.0, end: 12.0),
+                SilenceRange(start: 25.0, end: 32.0)
+            ]
+        }
+        
+        let task = Task.detached(priority: .userInitiated) { () -> [SilenceRange] in
+            guard let audioFile = try? AVAudioFile(forReading: audioURL) else {
+                return []
+            }
+            
+            let format = audioFile.processingFormat
+            let sampleRate = format.sampleRate
+            guard sampleRate > 0 else { return [] }
+            
+            // Buffer size of ~1.0 second
+            let bufferSize = AVAudioFrameCount(sampleRate)
+            guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: bufferSize) else {
+                return []
+            }
+            
+            var silentRanges: [SilenceRange] = []
+            var isSilent = false
+            var silenceStart: TimeInterval = 0
+            
+            // Sub-chunk size of 100ms
+            let subChunkSize = Int(sampleRate * 0.1)
+            
+            do {
+                while audioFile.framePosition < audioFile.length {
+                    let framesToRead = min(bufferSize, AVAudioFrameCount(audioFile.length - audioFile.framePosition))
+                    if framesToRead <= 0 { break }
+                    
+                    try audioFile.read(into: buffer, frameCount: framesToRead)
+                    
+                    guard let floatChannelData = buffer.floatChannelData else { continue }
+                    let channelData = floatChannelData[0]
+                    let frameLength = Int(buffer.frameLength)
+                    
+                    var offset = 0
+                    while offset < frameLength {
+                        let currentSubChunkSize = min(subChunkSize, frameLength - offset)
+                        if currentSubChunkSize <= 0 { break }
+                        
+                        var sum: Float = 0
+                        for i in 0..<currentSubChunkSize {
+                            let sample = channelData[offset + i]
+                            sum += sample * sample
+                        }
+                        
+                        let rms = sqrt(sum / Float(currentSubChunkSize))
+                        let db = rms > 0 ? 20 * log10(rms) : -100.0
+                        
+                        // Calculate absolute time of this sub-chunk
+                        let absoluteFramePosition = audioFile.framePosition - Int64(frameLength) + Int64(offset)
+                        let timeInSeconds = Double(absoluteFramePosition) / sampleRate
+                        
+                        let isChunkSilent = db < thresholdDB
+                        
+                        if isChunkSilent {
+                            if !isSilent {
+                                isSilent = true
+                                silenceStart = timeInSeconds
+                            }
+                        } else {
+                            if isSilent {
+                                isSilent = false
+                                let silenceEnd = timeInSeconds
+                                if silenceEnd - silenceStart >= minDuration {
+                                    silentRanges.append(SilenceRange(start: silenceStart, end: silenceEnd))
+                                }
+                            }
+                        }
+                        
+                        offset += subChunkSize
+                    }
+                }
+                
+                // If still silent at the end of the file
+                if isSilent {
+                    let silenceEnd = Double(audioFile.length) / sampleRate
+                    if silenceEnd - silenceStart >= minDuration {
+                        silentRanges.append(SilenceRange(start: silenceStart, end: silenceEnd))
+                    }
+                }
+            } catch {
+                print("[SilenceDetector] Error reading audio file: \(error.localizedDescription)")
+            }
+            
+            print("[SilenceDetector] Detection complete. Found \(silentRanges.count) silence ranges.")
+            return silentRanges
+        }
+        return await task.value
+    }
+}

+ 146 - 0
CelestiaTrace/Services/Auth/AuthManager.swift

@@ -0,0 +1,146 @@
+import Foundation
+import Combine
+
+/// Manages authentication state, user session, profile management, and audio sync access authorization.
+final class AuthManager: ObservableObject {
+    static let shared = AuthManager()
+
+    @Published private(set) var currentUser: UserAccount?
+    @Published var isProcessing: Bool = false
+    @Published var authErrorMessage: String?
+    
+    @Published var isSyncing: Bool = false
+    @Published var lastSyncDate: Date?
+    @Published var syncErrorMessage: String?
+    
+    var isLoggedIn: Bool {
+        currentUser != nil
+    }
+    
+    /// Auth repository interface. Can be swapped for `RemoteAuthServiceStub()` or mock services.
+    private let authService: AuthServiceProtocol
+    
+    init(authService: AuthServiceProtocol = LocalAuthRepository()) {
+        self.authService = authService
+        self.currentUser = authService.getCurrentUser()
+    }
+    
+    // MARK: - Auth Actions
+    
+    /// User login via username/email/phone & password.
+    func login(identifier: String, password: String, completion: ((Bool, String?) -> Void)? = nil) {
+        isProcessing = true
+        authErrorMessage = nil
+        
+        let req = LoginRequest(identifier: identifier, password: password)
+        authService.login(request: req) { [weak self] result in
+            DispatchQueue.main.async {
+                guard let self = self else { return }
+                self.isProcessing = false
+                switch result {
+                case .success(let user):
+                    self.currentUser = user
+                    completion?(true, nil)
+                case .failure(let error):
+                    self.authErrorMessage = error.localizedDescription
+                    completion?(false, error.localizedDescription)
+                }
+            }
+        }
+    }
+    
+    /// User registration.
+    func register(username: String, identifier: String, password: String, completion: ((Bool, String?) -> Void)? = nil) {
+        isProcessing = true
+        authErrorMessage = nil
+        
+        let req = RegisterRequest(username: username, identifier: identifier, password: password)
+        authService.register(request: req) { [weak self] result in
+            DispatchQueue.main.async {
+                guard let self = self else { return }
+                self.isProcessing = false
+                switch result {
+                case .success(let user):
+                    self.currentUser = user
+                    completion?(true, nil)
+                case .failure(let error):
+                    self.authErrorMessage = error.localizedDescription
+                    completion?(false, error.localizedDescription)
+                }
+            }
+        }
+    }
+    
+    /// Update profile information (nickname, email, phone, avatar).
+    func updateProfile(username: String?, email: String?, phoneNumber: String?, avatarURL: String?, completion: ((Bool, String?) -> Void)? = nil) {
+        isProcessing = true
+        authErrorMessage = nil
+        
+        let req = UpdateProfileRequest(username: username, email: email, phoneNumber: phoneNumber, avatarURL: avatarURL)
+        authService.updateProfile(request: req) { [weak self] result in
+            DispatchQueue.main.async {
+                guard let self = self else { return }
+                self.isProcessing = false
+                switch result {
+                case .success(let user):
+                    self.currentUser = user
+                    completion?(true, nil)
+                case .failure(let error):
+                    self.authErrorMessage = error.localizedDescription
+                    completion?(false, error.localizedDescription)
+                }
+            }
+        }
+    }
+    
+    /// Change password.
+    func changePassword(oldPassword: String, newPassword: String, completion: ((Bool, String?) -> Void)? = nil) {
+        isProcessing = true
+        authErrorMessage = nil
+        
+        let req = ChangePasswordRequest(oldPassword: oldPassword, newPassword: newPassword)
+        authService.changePassword(request: req) { [weak self] result in
+            DispatchQueue.main.async {
+                guard let self = self else { return }
+                self.isProcessing = false
+                switch result {
+                case .success:
+                    completion?(true, nil)
+                case .failure(let error):
+                    self.authErrorMessage = error.localizedDescription
+                    completion?(false, error.localizedDescription)
+                }
+            }
+        }
+    }
+    
+    /// Logout current user.
+    func logout() {
+        authService.logout { [weak self] in
+            DispatchQueue.main.async {
+                self?.currentUser = nil
+            }
+        }
+    }
+    
+    // MARK: - Audio Sync
+    
+    /// Triggers audio synchronization. Requires authenticated user.
+    func syncRecordings(onRequireAuth: @escaping () -> Void, onComplete: @escaping (Bool) -> Void) {
+        guard isLoggedIn else {
+            onRequireAuth()
+            return
+        }
+        
+        isSyncing = true
+        syncErrorMessage = nil
+        
+        // Mock sync delay
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak self] in
+            guard let self = self else { return }
+            self.isSyncing = false
+            self.lastSyncDate = Date()
+            onComplete(true)
+        }
+    }
+}

+ 75 - 0
CelestiaTrace/Services/Auth/AuthServiceProtocol.swift

@@ -0,0 +1,75 @@
+import Foundation
+
+/// Result enum representing outcomes of authentication operations.
+enum AuthResult<T> {
+    case success(T)
+    case failure(AuthError)
+}
+
+/// Errors that can occur during authentication operations.
+enum AuthError: Error, LocalizedError, Equatable {
+    case invalidInput(String)
+    case userAlreadyExists
+    case invalidCredentials
+    case userNotFound
+    case incorrectPassword
+    case storageError
+    case networkError(String)
+
+    var errorDescription: String? {
+        switch self {
+        case .invalidInput(let reason):
+            return reason
+        case .userAlreadyExists:
+            return "账号已存在,请直接登录"
+        case .invalidCredentials:
+            return "账号或密码错误"
+        case .userNotFound:
+            return "未找到对应账号"
+        case .incorrectPassword:
+            return "原密码校验错误"
+        case .storageError:
+            return "本地存储操作异常"
+        case .networkError(let msg):
+            return "网络连接失败: \(msg)"
+        }
+    }
+}
+
+/// Login request transfer object.
+struct LoginRequest {
+    let identifier: String
+    let password: String
+}
+
+/// Registration request transfer object.
+struct RegisterRequest {
+    let username: String
+    let identifier: String
+    let password: String
+}
+
+/// Profile update transfer object.
+struct UpdateProfileRequest {
+    let username: String?
+    let email: String?
+    let phoneNumber: String?
+    let avatarURL: String?
+}
+
+/// Password modification transfer object.
+struct ChangePasswordRequest {
+    let oldPassword: String
+    let newPassword: String
+}
+
+/// Abstract contract for authentication service implementations.
+/// Allows swapping between local persistence and remote server HTTP API seamlessly.
+protocol AuthServiceProtocol {
+    func login(request: LoginRequest, completion: @escaping (AuthResult<UserAccount>) -> Void)
+    func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void)
+    func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult<UserAccount>) -> Void)
+    func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult<Bool>) -> Void)
+    func logout(completion: @escaping () -> Void)
+    func getCurrentUser() -> UserAccount?
+}

+ 203 - 0
CelestiaTrace/Services/Auth/LocalAuthRepository.swift

@@ -0,0 +1,203 @@
+import Foundation
+
+/// Internal struct to persist user credentials locally.
+private struct LocalUserRecord: Codable {
+    var user: UserAccount
+    var passwordHash: String
+}
+
+/// Concrete local implementation of `AuthServiceProtocol`.
+/// Handles registration, authentication, profile updates, and password changes purely in local storage (UserDefaults / sandbox).
+final class LocalAuthRepository: AuthServiceProtocol {
+    
+    private let usersKey = "com.celestia.trace.registered_users_db"
+    private let currentUserKey = "com.celestia.trace.current_user_session"
+    
+    init() {}
+    
+    // MARK: - AuthServiceProtocol
+    
+    func login(request: LoginRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        let identifier = request.identifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+        let password = request.password
+        
+        guard !identifier.isEmpty, password.count >= 6 else {
+            completion(.failure(.invalidInput("请输入有效账号与不少于 6 位的密码")))
+            return
+        }
+        
+        var records = loadAllRecords()
+        
+        // Match identifier by email, phone, or matching username/id
+        guard let matchingIndex = records.firstIndex(where: { rec in
+            rec.user.email?.lowercased() == identifier ||
+            rec.user.phoneNumber == identifier ||
+            rec.user.username.lowercased() == identifier ||
+            rec.user.id == identifier
+        }) else {
+            // Auto-fallback for demo convenience if no users registered yet, or error out:
+            completion(.failure(.invalidCredentials))
+            return
+        }
+        
+        let record = records[matchingIndex]
+        if record.passwordHash == hashPassword(password) {
+            var updatedUser = record.user
+            updatedUser.token = "jwt_token_" + UUID().uuidString.prefix(12)
+            records[matchingIndex].user = updatedUser
+            saveAllRecords(records)
+            saveCurrentSession(updatedUser)
+            completion(.success(updatedUser))
+        } else {
+            completion(.failure(.invalidCredentials))
+        }
+    }
+    
+    func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        let username = request.username.trimmingCharacters(in: .whitespacesAndNewlines)
+        let identifier = request.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
+        let password = request.password
+        
+        guard !username.isEmpty else {
+            completion(.failure(.invalidInput("请输入用户名/昵称")))
+            return
+        }
+        
+        guard !identifier.isEmpty else {
+            completion(.failure(.invalidInput("请输入手机号或邮箱")))
+            return
+        }
+        
+        guard password.count >= 6 else {
+            completion(.failure(.invalidInput("密码长度不能小于 6 位")))
+            return
+        }
+        
+        var records = loadAllRecords()
+        let identifierLower = identifier.lowercased()
+        
+        let alreadyExists = records.contains { rec in
+            rec.user.email?.lowercased() == identifierLower ||
+            rec.user.phoneNumber == identifierLower ||
+            rec.user.username.lowercased() == username.lowercased()
+        }
+        
+        if alreadyExists {
+            completion(.failure(.userAlreadyExists))
+            return
+        }
+        
+        let isEmail = identifier.contains("@")
+        let newUser = UserAccount(
+            id: "usr_" + UUID().uuidString.prefix(8),
+            username: username,
+            email: isEmail ? identifier : nil,
+            phoneNumber: isEmail ? nil : identifier,
+            avatarURL: "preset_avatar_\((records.count % 5) + 1)",
+            token: "jwt_token_" + UUID().uuidString.prefix(12),
+            registeredAt: Date()
+        )
+        
+        let record = LocalUserRecord(user: newUser, passwordHash: hashPassword(password))
+        records.append(record)
+        
+        saveAllRecords(records)
+        saveCurrentSession(newUser)
+        completion(.success(newUser))
+    }
+    
+    func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        guard var currentUser = getCurrentUser() else {
+            completion(.failure(.userNotFound))
+            return
+        }
+        
+        if let username = request.username, !username.trimmingCharacters(in: .whitespaces).isEmpty {
+            currentUser.username = username
+        }
+        if let email = request.email {
+            currentUser.email = email.isEmpty ? nil : email
+        }
+        if let phone = request.phoneNumber {
+            currentUser.phoneNumber = phone.isEmpty ? nil : phone
+        }
+        if let avatar = request.avatarURL {
+            currentUser.avatarURL = avatar
+        }
+        
+        var records = loadAllRecords()
+        if let idx = records.firstIndex(where: { $0.user.id == currentUser.id }) {
+            records[idx].user = currentUser
+            saveAllRecords(records)
+        }
+        
+        saveCurrentSession(currentUser)
+        completion(.success(currentUser))
+    }
+    
+    func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult<Bool>) -> Void) {
+        guard let currentUser = getCurrentUser() else {
+            completion(.failure(.userNotFound))
+            return
+        }
+        
+        guard request.newPassword.count >= 6 else {
+            completion(.failure(.invalidInput("新密码长度不能少于 6 位")))
+            return
+        }
+        
+        var records = loadAllRecords()
+        guard let idx = records.firstIndex(where: { $0.user.id == currentUser.id }) else {
+            completion(.failure(.userNotFound))
+            return
+        }
+        
+        if records[idx].passwordHash != hashPassword(request.oldPassword) {
+            completion(.failure(.incorrectPassword))
+            return
+        }
+        
+        records[idx].passwordHash = hashPassword(request.newPassword)
+        saveAllRecords(records)
+        completion(.success(true))
+    }
+    
+    func logout(completion: @escaping () -> Void) {
+        UserDefaults.standard.removeObject(forKey: currentUserKey)
+        completion()
+    }
+    
+    func getCurrentUser() -> UserAccount? {
+        guard let data = UserDefaults.standard.data(forKey: currentUserKey),
+              let user = try? JSONDecoder().decode(UserAccount.self, from: data) else {
+            return nil
+        }
+        return user
+    }
+    
+    // MARK: - Private Helpers
+    
+    private func loadAllRecords() -> [LocalUserRecord] {
+        guard let data = UserDefaults.standard.data(forKey: usersKey),
+              let records = try? JSONDecoder().decode([LocalUserRecord].self, from: data) else {
+            return []
+        }
+        return records
+    }
+    
+    private func saveAllRecords(_ records: [LocalUserRecord]) {
+        if let data = try? JSONEncoder().encode(records) {
+            UserDefaults.standard.set(data, forKey: usersKey)
+        }
+    }
+    
+    private func saveCurrentSession(_ user: UserAccount) {
+        if let data = try? JSONEncoder().encode(user) {
+            UserDefaults.standard.set(data, forKey: currentUserKey)
+        }
+    }
+    
+    private func hashPassword(_ raw: String) -> String {
+        return "hashed_" + String(raw.reversed()) + "_salt"
+    }
+}

+ 47 - 0
CelestiaTrace/Services/Auth/RemoteAuthServiceStub.swift

@@ -0,0 +1,47 @@
+import Foundation
+
+/// Remote server adapter placeholder implementing `AuthServiceProtocol`.
+/// This stub demonstrates how `CelestiaTrace` can switch to a real remote backend server in the future.
+final class RemoteAuthServiceStub: AuthServiceProtocol {
+    private let baseURL: URL
+    private let session: URLSession
+    
+    init(baseURL: URL = URL(string: "https://api.celestiatrace.com/v1")!, session: URLSession = .shared) {
+        self.baseURL = baseURL
+        self.session = session
+    }
+    
+    func login(request: LoginRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        // TODO: Replace stub with URLSession POST request to `/auth/login`
+        // let endpoint = baseURL.appendingPathComponent("auth/login")
+        // var urlReq = URLRequest(url: endpoint)
+        // urlReq.httpMethod = "POST"
+        // ...
+        
+        completion(.failure(.networkError("未配置实际远程服务器地址")))
+    }
+    
+    func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        // TODO: Replace stub with URLSession POST request to `/auth/register`
+        completion(.failure(.networkError("未配置实际远程服务器地址")))
+    }
+    
+    func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
+        // TODO: Replace stub with URLSession PUT request to `/user/profile`
+        completion(.failure(.networkError("未配置实际远程服务器地址")))
+    }
+    
+    func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult<Bool>) -> Void) {
+        // TODO: Replace stub with URLSession POST request to `/user/change-password`
+        completion(.failure(.networkError("未配置实际远程服务器地址")))
+    }
+    
+    func logout(completion: @escaping () -> Void) {
+        // TODO: Call remote `/auth/logout` endpoint if needed
+        completion()
+    }
+    
+    func getCurrentUser() -> UserAccount? {
+        return nil
+    }
+}

+ 127 - 0
CelestiaTrace/Services/Bluetooth/BLEManager.swift

@@ -0,0 +1,127 @@
+import Foundation
+import CoreBluetooth
+import Combine
+
+/// Manages Bluetooth Low Energy (BLE) scanning, device connection, and binding to user accounts.
+final class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
+    static let shared = BLEManager()
+
+    @Published private(set) var state: CBManagerState = .unknown
+    @Published var isScanning: Bool = false
+    @Published var discoveredDevices: [DiscoveredBLEDevice] = []
+    @Published var boundDevices: [BoundDevice] = []
+    
+    private var centralManager: CBCentralManager!
+    private let boundDevicesKey = "com.celestia.trace.bound_devices"
+    
+    override private init() {
+        super.init()
+        self.centralManager = CBCentralManager(delegate: self, queue: nil)
+        loadBoundDevices()
+    }
+    
+    // MARK: - CentralManager Delegate
+    
+    func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        DispatchQueue.main.async {
+            self.state = central.state
+        }
+    }
+    
+    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
+        let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "现场录音设备"
+        let device = DiscoveredBLEDevice(
+            id: peripheral.identifier.uuidString,
+            name: name,
+            rssi: RSSI.intValue,
+            peripheralUUID: peripheral.identifier.uuidString
+        )
+        
+        DispatchQueue.main.async {
+            if !self.discoveredDevices.contains(where: { $0.id == device.id }) {
+                self.discoveredDevices.append(device)
+            }
+        }
+    }
+    
+    // MARK: - Scanning & Mocking
+    
+    func startScanning() {
+        discoveredDevices.removeAll()
+        isScanning = true
+        
+        if centralManager.state == .poweredOn {
+            centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
+        }
+        
+        // Populate mock devices for simulator or testing environment
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
+            guard let self = self, self.isScanning else { return }
+            let mockDevices = [
+                DiscoveredBLEDevice(id: "MOCK-BLE-01", name: "星痕专业录音麦克风 01", rssi: -58, peripheralUUID: "0000180A-0000-1000-8000-00805F9B34FB"),
+                DiscoveredBLEDevice(id: "MOCK-BLE-02", name: "星痕无线音频集线器", rssi: -72, peripheralUUID: "0000180F-0000-1000-8000-00805F9B34FB"),
+                DiscoveredBLEDevice(id: "MOCK-BLE-03", name: "智能胸卡录音器 A2", rssi: -85, peripheralUUID: "0000181A-0000-1000-8000-00805F9B34FB")
+            ]
+            for device in mockDevices {
+                if !self.discoveredDevices.contains(where: { $0.id == device.id }) {
+                    self.discoveredDevices.append(device)
+                }
+            }
+        }
+    }
+    
+    func stopScanning() {
+        if centralManager.state == .poweredOn {
+            centralManager.stopScan()
+        }
+        isScanning = false
+    }
+    
+    // MARK: - Binding Management
+    
+    /// Binds a discovered BLE device to the specified user account ID.
+    func bindDevice(_ device: DiscoveredBLEDevice, userId: String) -> BoundDevice {
+        let boundDevice = BoundDevice(
+            id: device.id,
+            name: device.name,
+            peripheralUUID: device.peripheralUUID,
+            boundAt: Date(),
+            isConnected: true,
+            batteryLevel: Int.random(in: 75...99),
+            firmwareVersion: "v1.4.2",
+            boundUserId: userId
+        )
+        
+        // Unbind any existing duplicate
+        boundDevices.removeAll(where: { $0.id == boundDevice.id })
+        boundDevices.append(boundDevice)
+        saveBoundDevices()
+        return boundDevice
+    }
+    
+    /// Unbinds a device by its ID.
+    func unbindDevice(id: String) {
+        boundDevices.removeAll(where: { $0.id == id })
+        saveBoundDevices()
+    }
+    
+    /// Returns devices bound to the given user account.
+    func devices(forUserId userId: String) -> [BoundDevice] {
+        return boundDevices.filter { $0.boundUserId == userId }
+    }
+    
+    // MARK: - Local Persistence
+    
+    private func saveBoundDevices() {
+        if let data = try? JSONEncoder().encode(boundDevices) {
+            UserDefaults.standard.set(data, forKey: boundDevicesKey)
+        }
+    }
+    
+    private func loadBoundDevices() {
+        if let data = UserDefaults.standard.data(forKey: boundDevicesKey),
+           let devices = try? JSONDecoder().decode([BoundDevice].self, from: data) {
+            self.boundDevices = devices
+        }
+    }
+}

+ 37 - 0
CelestiaTrace/Services/HapticManager.swift

@@ -0,0 +1,37 @@
+import UIKit
+
+/// Centralized haptic feedback manager for CelestiaTrace.
+enum HapticManager {
+    enum HapticType {
+        case recordStart
+        case recordStop
+        case photoCapture
+        case noteAdded
+        case tapFeedback
+    }
+    
+    static func trigger(_ type: HapticType) {
+        switch type {
+        case .recordStart:
+            let generator = UIImpactFeedbackGenerator(style: .heavy)
+            generator.prepare()
+            generator.impactOccurred()
+        case .recordStop:
+            let generator = UINotificationFeedbackGenerator()
+            generator.prepare()
+            generator.notificationOccurred(.success)
+        case .photoCapture:
+            let generator = UIImpactFeedbackGenerator(style: .medium)
+            generator.prepare()
+            generator.impactOccurred()
+        case .noteAdded:
+            let generator = UIImpactFeedbackGenerator(style: .light)
+            generator.prepare()
+            generator.impactOccurred()
+        case .tapFeedback:
+            let generator = UIImpactFeedbackGenerator(style: .soft)
+            generator.prepare()
+            generator.impactOccurred()
+        }
+    }
+}

+ 62 - 0
CelestiaTrace/Services/Network/MockNetworkService.swift

@@ -0,0 +1,62 @@
+import Foundation
+import Combine
+
+/// Mock network service simulating cloud sync progress and AI report generation.
+final class MockNetworkService: NetworkServiceProtocol {
+    // MARK: - NetworkServiceProtocol
+    
+    func syncSession(_ sessionId: UUID) -> AnyPublisher<Double, Error> {
+        // Simulate upload progress from 0.0 to 1.0 over ~3 seconds
+        let totalSteps = 30
+        let interval: TimeInterval = 0.1
+        
+        return Timer.publish(every: interval, on: .main, in: .common)
+            .autoconnect()
+            .scan(0) { count, _ in count + 1 }
+            .map { step in
+                min(Double(step) / Double(totalSteps), 1.0)
+            }
+            .prefix(while: { $0 <= 1.0 })
+            .append(1.0)
+            .removeDuplicates()
+            .setFailureType(to: Error.self)
+            .eraseToAnyPublisher()
+    }
+    
+    func fetchAIReport(for cloudSessionId: String) async throws -> String {
+        // Simulate network latency
+        try await Task.sleep(for: .seconds(1))
+        
+        return """
+        # 田野调查 AI 分析报告
+        
+        **会话编号**: \(cloudSessionId)
+        **生成时间**: \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))
+        
+        ## 摘要
+        
+        本次田野调查共记录音频片段 3 段,拍摄现场照片 5 张,添加文字笔记 8 条。调查地点位于城市边缘的工业遗址区域,\
+        主要关注废弃厂房的建筑结构保存状况及周边生态恢复情况。
+        
+        ## 关键发现
+        
+        1. **建筑结构**:主厂房钢结构框架保存完好,屋顶覆盖层已大面积脱落。东侧附属建筑存在明显沉降迹象。
+        2. **生态恢复**:厂区内部已有大量自生植被覆盖,观察到至少 3 种本地鸟类在废弃管道中筑巢。
+        3. **环境风险**:西北角地面检测到轻微土壤污染痕迹,建议后续进行专业采样分析。
+        
+        ## 音频转写片段
+        
+        > "……这里的钢梁连接处可以看到明显的铆接工艺,这是典型的上世纪八十年代的建造方式。注意看这个节点,\
+        焊接补强的痕迹说明后期做过结构加固……"
+        
+        ## 建议后续行动
+        
+        - 对西北角区域进行土壤采样送检
+        - 联系当地档案馆获取原始建筑图纸
+        - 安排无人机航拍以获取完整厂区俯瞰影像
+        
+        ---
+        *此报告由星痕现场记录 AI 引擎自动生成,仅供参考。*
+        """
+    }
+}

+ 13 - 0
CelestiaTrace/Services/Network/NetworkServiceProtocol.swift

@@ -0,0 +1,13 @@
+import Foundation
+import Combine
+
+/// Reserved protocol for cloud sync with Celestia Track backend.
+protocol NetworkServiceProtocol {
+    /// Upload a complete research session including audio, photos, notes and timeline JSON.
+    /// - Returns: Publisher emitting upload progress (0.0 - 1.0)
+    func syncSession(_ sessionId: UUID) -> AnyPublisher<Double, Error>
+    
+    /// Fetch AI transcription and report generation status.
+    /// - Returns: Transcribed text or AI summary report
+    func fetchAIReport(for cloudSessionId: String) async throws -> String
+}

+ 369 - 0
CelestiaTrace/ViewModels/PlaybackViewModel.swift

@@ -0,0 +1,369 @@
+import SwiftUI
+import Observation
+import AVFoundation
+
+// MARK: - PlaybackViewModel
+
+/// Manages audio playback state, timeline scrubbing, and event filtering
+/// for session detail/review screens using AVAudioPlayer for real audio output.
+@Observable
+final class PlaybackViewModel: NSObject, AVAudioPlayerDelegate {
+
+    // MARK: - Playback State
+
+    /// Current playback position in milliseconds.
+    var currentPlaybackTimeMs: Double = 0
+
+    /// Whether audio is currently playing.
+    private(set) var isPlaying: Bool = false
+
+    /// Total session duration in milliseconds.
+    var totalDurationMs: Double = 0
+    
+    // MARK: - Silence Skip State
+    
+    /// Whether auto-skip silence is enabled
+    var isSilenceSkipEnabled: Bool = false
+    
+    /// Whether background silence analysis is active
+    var isAnalyzingSilence: Bool = false
+    
+    /// Detected silent intervals
+    var silentRanges: [SilenceRange] = []
+
+    // MARK: - Audio Player Private State
+    private var audioPlayer: AVAudioPlayer?
+    private var activeAudioURL: URL?
+
+    // MARK: - Computed Properties
+
+    /// Formats the current playback position as `MM:SS`.
+    var currentTimeFormatted: String {
+        let totalSeconds = Int(currentPlaybackTimeMs / 1000)
+        let minutes = totalSeconds / 60
+        let seconds = totalSeconds % 60
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+
+    /// Total duration formatted as `MM:SS`.
+    var totalTimeFormatted: String {
+        let totalSeconds = Int(totalDurationMs / 1000)
+        let minutes = totalSeconds / 60
+        let seconds = totalSeconds % 60
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+
+    /// Playback progress as a normalized value (0.0 – 1.0).
+    var progress: Double {
+        guard totalDurationMs > 0 else { return 0 }
+        return min(max(currentPlaybackTimeMs / totalDurationMs, 0), 1)
+    }
+
+    // MARK: - Private
+
+    /// Tick timer that advances playback position at ~50ms intervals.
+    private var playbackTimer: Timer?
+
+    /// Tick interval in seconds (≈20 ticks/sec for smooth scrubber movement).
+    private let tickInterval: TimeInterval = 0.05
+
+    // MARK: - Init / Deinit
+
+    override init() {
+        super.init()
+    }
+
+    deinit {
+        playbackTimer?.invalidate()
+        audioPlayer?.stop()
+    }
+
+    // MARK: - Player Preparation
+
+    /// Prepares the audio player for the given file path or generates a sample audio fallback.
+    func preparePlayer(path: String?, durationMs: Double = 0) {
+        stopPlaybackTimer()
+        audioPlayer?.stop()
+        audioPlayer = nil
+
+        let urlToPlay: URL?
+        if let path = path, !path.isEmpty, FileManager.default.fileExists(atPath: path) {
+            urlToPlay = URL(fileURLWithPath: path)
+        } else {
+            let targetDuration = durationMs > 0 ? durationMs / 1000.0 : 30.0
+            urlToPlay = generateFallbackAudioFile(durationSeconds: targetDuration)
+        }
+
+        guard let targetURL = urlToPlay else { return }
+        self.activeAudioURL = targetURL
+
+        setupAudioSession()
+
+        do {
+            let player = try AVAudioPlayer(contentsOf: targetURL)
+            player.delegate = self
+            player.prepareToPlay()
+            player.volume = 1.0
+            self.audioPlayer = player
+            if durationMs <= 0 {
+                self.totalDurationMs = player.duration * 1000.0
+            } else {
+                self.totalDurationMs = durationMs
+            }
+        } catch {
+            print("[PlaybackViewModel] Failed to initialize AVAudioPlayer: \(error.localizedDescription)")
+        }
+    }
+
+    private func setupAudioSession() {
+        let session = AVAudioSession.sharedInstance()
+        do {
+            try session.setCategory(.playback, mode: .default, options: [.defaultToSpeaker])
+            try session.setActive(true)
+        } catch {
+            print("[PlaybackViewModel] Failed to setup AVAudioSession: \(error.localizedDescription)")
+        }
+    }
+
+    // MARK: - Playback Controls
+
+    /// Starts or resumes playback from the current position.
+    func play() {
+        guard !isPlaying else { return }
+
+        setupAudioSession()
+
+        if let player = audioPlayer {
+            player.currentTime = currentPlaybackTimeMs / 1000.0
+            player.play()
+        }
+
+        isPlaying = true
+        startPlaybackTimer()
+    }
+
+    /// Pauses playback at the current position.
+    func pause() {
+        guard isPlaying else { return }
+        audioPlayer?.pause()
+        isPlaying = false
+        stopPlaybackTimer()
+    }
+
+    /// Toggles between playing and paused states.
+    func togglePlayback() {
+        if isPlaying {
+            pause()
+        } else {
+            play()
+        }
+    }
+
+    /// Seeks to a specific position in the timeline.
+    /// - Parameter timeMs: The target position in milliseconds.
+    func seekTo(timeMs: Double) {
+        let target = min(max(timeMs, 0), totalDurationMs)
+        currentPlaybackTimeMs = target
+        if let player = audioPlayer {
+            player.currentTime = target / 1000.0
+        }
+    }
+
+    /// Convenience overload accepting Int64.
+    func seekTo(timeMs: Int64) {
+        seekTo(timeMs: Double(timeMs))
+    }
+
+    /// Seeks to a normalized progress value (0.0 – 1.0).
+    /// - Parameter progress: Normalized position.
+    func seekToProgress(_ progress: Double) {
+        let clampedProgress = min(max(progress, 0), 1)
+        seekTo(timeMs: clampedProgress * totalDurationMs)
+    }
+
+    /// Resets playback to the beginning.
+    func reset() {
+        pause()
+        seekTo(timeMs: 0.0)
+    }
+
+    // MARK: - AVAudioPlayerDelegate
+
+    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        isPlaying = false
+        currentPlaybackTimeMs = totalDurationMs
+        stopPlaybackTimer()
+    }
+
+    // MARK: - Private Timer & Silence Detection
+
+    private func startPlaybackTimer() {
+        playbackTimer?.invalidate()
+        playbackTimer = Timer.scheduledTimer(
+            withTimeInterval: tickInterval,
+            repeats: true
+        ) { [weak self] _ in
+            guard let self else { return }
+            self.tick()
+        }
+        if let timer = playbackTimer {
+            RunLoop.main.add(timer, forMode: .common)
+        }
+    }
+
+    private func stopPlaybackTimer() {
+        playbackTimer?.invalidate()
+        playbackTimer = nil
+    }
+
+    /// Runs silence detection asynchronously.
+    func analyzeSilence(audioURL: URL?) {
+        let url = audioURL ?? activeAudioURL
+        guard let url = url else {
+            self.silentRanges = [
+                SilenceRange(start: 5.0, end: 12.0),
+                SilenceRange(start: 25.0, end: 32.0)
+            ]
+            return
+        }
+        
+        isAnalyzingSilence = true
+        Task {
+            let ranges = await SilenceDetector.detectSilence(in: url)
+            await MainActor.run {
+                self.silentRanges = ranges
+                self.isAnalyzingSilence = false
+            }
+        }
+    }
+    
+    private func checkForSilenceAndSkip() {
+        guard isSilenceSkipEnabled && !silentRanges.isEmpty else { return }
+        
+        let currentTimeSec = currentPlaybackTimeMs / 1000.0
+        for range in silentRanges {
+            if currentTimeSec >= range.start && currentTimeSec < range.end {
+                let targetMs = range.end * 1000.0
+                seekTo(timeMs: targetMs)
+                print("[PlaybackViewModel] Auto-skipped silence from \(range.start)s to \(range.end)s")
+                break
+            }
+        }
+    }
+
+    /// Advances playback by one tick interval, syncing with audioPlayer if active.
+    private func tick() {
+        if let player = audioPlayer, player.isPlaying {
+            currentPlaybackTimeMs = player.currentTime * 1000.0
+        } else {
+            currentPlaybackTimeMs += tickInterval * 1000
+        }
+        
+        checkForSilenceAndSkip()
+
+        if currentPlaybackTimeMs >= totalDurationMs {
+            currentPlaybackTimeMs = totalDurationMs
+            pause()
+        }
+    }
+
+    // MARK: - Event Queries
+
+    func sortedEvents(from events: [CelestiaTimelineEvent]) -> [CelestiaTimelineEvent] {
+        events.sorted { $0.relativeTimeMs < $1.relativeTimeMs }
+    }
+
+    func sortedEvents(from session: CelestiaSession) -> [CelestiaTimelineEvent] {
+        sortedEvents(from: session.events)
+    }
+
+    func filteredEvents(
+        from events: [CelestiaTimelineEvent],
+        type: String?
+    ) -> [CelestiaTimelineEvent] {
+        let filtered: [CelestiaTimelineEvent]
+        if let type {
+            filtered = events.filter { $0.eventType == type }
+        } else {
+            filtered = events
+        }
+        return filtered.sorted { $0.relativeTimeMs < $1.relativeTimeMs }
+    }
+
+    func nearestEvent(
+        from events: [CelestiaTimelineEvent],
+        toleranceMs: Double = 500
+    ) -> CelestiaTimelineEvent? {
+        events
+            .min(by: {
+                abs(Double($0.relativeTimeMs) - currentPlaybackTimeMs)
+                    < abs(Double($1.relativeTimeMs) - currentPlaybackTimeMs)
+            })
+            .flatMap { event in
+                abs(Double(event.relativeTimeMs) - currentPlaybackTimeMs) <= toleranceMs
+                    ? event
+                    : nil
+            }
+    }
+
+    // MARK: - Helper Tone Generator
+
+    private func generateFallbackAudioFile(durationSeconds: Double) -> URL? {
+        let tempDir = FileManager.default.temporaryDirectory
+        let fileURL = tempDir.appendingPathComponent("sample_playback.wav")
+        if FileManager.default.fileExists(atPath: fileURL.path) {
+            return fileURL
+        }
+
+        let sampleRate: Double = 22050.0
+        let numSamples = Int(sampleRate * durationSeconds)
+        let numChannels: UInt16 = 1
+        let bitsPerSample: UInt16 = 16
+        let byteRate = UInt32(Double(sampleRate) * Double(numChannels) * Double(bitsPerSample / 8))
+        let blockAlign = UInt16(numChannels * (bitsPerSample / 8))
+        let dataSize = UInt32(numSamples * Int(blockAlign))
+        let chunkSize = 36 + dataSize
+
+        var data = Data()
+
+        // RIFF header
+        data.append(contentsOf: Array("RIFF".utf8))
+        data.append(withUnsafeBytes(of: chunkSize.littleEndian) { Data($0) })
+        data.append(contentsOf: Array("WAVE".utf8))
+
+        // fmt chunk
+        data.append(contentsOf: Array("fmt ".utf8))
+        data.append(withUnsafeBytes(of: UInt32(16).littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: UInt16(1).littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: numChannels.littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: UInt32(sampleRate).littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: byteRate.littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: blockAlign.littleEndian) { Data($0) })
+        data.append(withUnsafeBytes(of: bitsPerSample.littleEndian) { Data($0) })
+
+        // data chunk
+        data.append(contentsOf: Array("data".utf8))
+        data.append(withUnsafeBytes(of: dataSize.littleEndian) { Data($0) })
+
+        let frequency1: Double = 440.0
+        let frequency2: Double = 554.37
+        for i in 0..<numSamples {
+            let t = Double(i) / sampleRate
+            let sampleVal1 = sin(2.0 * .pi * frequency1 * t)
+            let sampleVal2 = sin(2.0 * .pi * frequency2 * t)
+            let envelope = min(1.0, min(t * 2.0, (durationSeconds - t) * 2.0))
+            let combined = (sampleVal1 * 0.4 + sampleVal2 * 0.3) * envelope
+            let pcmValue = Int16(clamping: Int(combined * 16384.0))
+            data.append(withUnsafeBytes(of: pcmValue.littleEndian) { Data($0) })
+        }
+
+        do {
+            try data.write(to: fileURL)
+            return fileURL
+        } catch {
+            print("[PlaybackViewModel] Failed to write fallback audio WAV: \(error.localizedDescription)")
+            return nil
+        }
+    }
+}
+

+ 156 - 0
CelestiaTrace/ViewModels/RecordingViewModel.swift

@@ -0,0 +1,156 @@
+import SwiftUI
+import SwiftData
+import Observation
+
+// MARK: - RecordingViewModel
+
+/// Manages recording state and bridges the ObservableObject-based AudioRecorder
+/// into the iOS 17 Observation framework via Timer-based polling.
+@Observable
+final class RecordingViewModel {
+
+    // MARK: - Published State
+
+    private(set) var isRecording: Bool = false
+    private(set) var elapsedTime: TimeInterval = 0
+    private(set) var currentAmplitude: Float = 0
+    private(set) var waveformSamples: [Float] = []
+    private(set) var isPaused: Bool = false
+
+    // MARK: - Computed Properties
+
+    /// Formats elapsed time as `HH:MM:SS`.
+    var elapsedTimeFormatted: String {
+        let totalSeconds = Int(elapsedTime)
+        let hours = totalSeconds / 3600
+        let minutes = (totalSeconds % 3600) / 60
+        let seconds = totalSeconds % 60
+        return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
+    }
+
+    /// Local URL where the audio file is stored.
+    var outputFileURL: URL? {
+        recorder.outputFileURL
+    }
+
+    // MARK: - Private
+
+    /// Concrete recorder instance conforming to AudioRecorderProtocol.
+    private let recorder: any AudioRecorderProtocol
+
+    /// Timer that syncs recorder state → ViewModel state at ~60 Hz.
+    private var syncTimer: Timer?
+
+    // MARK: - Init
+
+    init() {
+        self.recorder = RealAudioRecorder()
+    }
+
+    init(recorder: any AudioRecorderProtocol) {
+        self.recorder = recorder
+    }
+
+    deinit {
+        syncTimer?.invalidate()
+    }
+
+    // MARK: - Recording Controls
+
+    /// Starts recording and begins polling the recorder for state updates.
+    func startRecording() {
+        recorder.startRecording()
+        isRecording = true
+        startSyncTimer()
+    }
+
+    /// Stops recording and cleans up the sync timer.
+    func stopRecording() {
+        recorder.stopRecording()
+        isRecording = false
+        stopSyncTimer()
+        syncState() // Final sync
+    }
+
+    /// Pauses the current recording session.
+    func pauseRecording() {
+        recorder.pauseRecording()
+        syncState()
+    }
+
+    /// Resumes a paused recording session.
+    func resumeRecording() {
+        recorder.resumeRecording()
+        syncState()
+    }
+
+    // MARK: - Timeline Event Creation
+
+    /// Creates a PHOTO timeline event attached to the given session.
+    /// - Parameter session: The active `CelestiaSession` to attach the event to.
+    /// - Returns: The newly created `CelestiaTimelineEvent`.
+    @discardableResult
+    func addPhotoEvent(to session: CelestiaSession, localFilePath: String? = nil) -> CelestiaTimelineEvent {
+        let relativeMs = Int64(elapsedTime * 1000)
+        let event = CelestiaTimelineEvent(relativeTimeMs: relativeMs, eventType: "PHOTO")
+        event.localFilePath = localFilePath
+        session.events.append(event)
+        return event
+    }
+
+    /// Creates a NOTE timeline event attached to the given session.
+    /// - Parameters:
+    ///   - session: The active `CelestiaSession` to attach the event to.
+    ///   - text: The note text content.
+    /// - Returns: The newly created `CelestiaTimelineEvent`.
+    @discardableResult
+    func addNoteEvent(to session: CelestiaSession, text: String) -> CelestiaTimelineEvent {
+        let relativeMs = Int64(elapsedTime * 1000)
+        let event = CelestiaTimelineEvent(relativeTimeMs: relativeMs, eventType: "NOTE")
+        event.textContent = text
+        session.events.append(event)
+        return event
+    }
+
+    /// Creates a MARKER timeline event attached to the given session.
+    /// - Parameters:
+    ///   - session: The active `CelestiaSession` to attach the event to.
+    ///   - text: The marker label text.
+    /// - Returns: The newly created `CelestiaTimelineEvent`.
+    @discardableResult
+    func addMarkerEvent(to session: CelestiaSession, text: String) -> CelestiaTimelineEvent {
+        let relativeMs = Int64(elapsedTime * 1000)
+        let event = CelestiaTimelineEvent(relativeTimeMs: relativeMs, eventType: "MARKER")
+        event.textContent = text
+        session.events.append(event)
+        return event
+    }
+
+    // MARK: - Private Sync Timer
+
+    private func startSyncTimer() {
+        syncTimer?.invalidate()
+        // ~60 Hz polling to bridge ObservableObject → @Observable
+        syncTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in
+            self?.syncState()
+        }
+        // Ensure timer fires during UI tracking (scrolling, etc.)
+        if let timer = syncTimer {
+            RunLoop.main.add(timer, forMode: .common)
+        }
+    }
+
+    private func stopSyncTimer() {
+        syncTimer?.invalidate()
+        syncTimer = nil
+    }
+
+    /// Pulls current values from the ObservableObject recorder into @Observable properties.
+    private func syncState() {
+        isRecording = recorder.isRecording
+        elapsedTime = recorder.elapsedTime
+        currentAmplitude = recorder.currentAmplitude
+        waveformSamples = recorder.waveformSamples
+        isPaused = recorder.isPaused
+    }
+}

+ 57 - 0
CelestiaTrace/Views/Components/DiscreetWarningBanner.swift

@@ -0,0 +1,57 @@
+import SwiftUI
+
+// MARK: - DiscreetWarningBanner
+
+/// A highly minimalist, wireframe-style warning banner that displays system warnings discretely.
+/// Can be dismissed by the user.
+struct DiscreetWarningBanner: View {
+    
+    /// Warning message to display.
+    let warning: String
+    
+    /// Triggered when the user taps the close button.
+    let onDismiss: () -> Void
+    
+    var body: some View {
+        HStack(spacing: 8) {
+            Image(systemName: "exclamationmark.triangle")
+                .font(.system(size: 11, weight: .light))
+                .foregroundColor(.primary)
+            
+            Text(warning)
+                .font(.system(size: 10, weight: .regular))
+                .foregroundColor(.secondary)
+                .lineLimit(1)
+            
+            Spacer()
+            
+            Button(action: onDismiss) {
+                Image(systemName: "xmark")
+                    .font(.system(size: 8, weight: .light))
+                    .foregroundColor(.secondary.opacity(0.7))
+                    .frame(width: 16, height: 16)
+            }
+            .buttonStyle(.plain)
+        }
+        .padding(.horizontal, 12)
+        .padding(.vertical, 8)
+        .background(Color.cardBackground.opacity(0.1))
+        .businessBorder(cornerRadius: 6)
+        .transition(.move(edge: .top).combined(with: .opacity))
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    VStack {
+        DiscreetWarningBanner(
+            warning: "⚠️ 检测到后台有其他音频播放中,建议关闭以防混入录音。"
+        ) {
+            print("Dismissed")
+        }
+        .padding()
+        Spacer()
+    }
+    .background(Color.spaceBlack)
+}

+ 45 - 0
CelestiaTrace/Views/Components/ImagePicker.swift

@@ -0,0 +1,45 @@
+import SwiftUI
+import UIKit
+
+/// A helper SwiftUI wrapper for UIImagePickerController to select photos or capture via camera.
+struct ImagePicker: UIViewControllerRepresentable {
+    var sourceType: UIImagePickerController.SourceType = .camera
+    var onImagePicked: (UIImage) -> Void
+
+    @Environment(\.dismiss) private var dismiss
+
+    func makeUIViewController(context: Context) -> UIImagePickerController {
+        let picker = UIImagePickerController()
+        picker.sourceType = sourceType
+        picker.delegate = context.coordinator
+        return picker
+    }
+
+    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator(self)
+    }
+
+    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+        let parent: ImagePicker
+
+        init(_ parent: ImagePicker) {
+            self.parent = parent
+        }
+
+        func imagePickerController(
+            _ picker: UIImagePickerController,
+            didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
+        ) {
+            if let image = info[.originalImage] as? UIImage {
+                parent.onImagePicked(image)
+            }
+            parent.dismiss()
+        }
+
+        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+            parent.dismiss()
+        }
+    }
+}

+ 79 - 0
CelestiaTrace/Views/Components/PulsingRecordButton.swift

@@ -0,0 +1,79 @@
+import SwiftUI
+
+// MARK: - PulsingRecordButton
+
+/// A minimalist, line-drawn record button suitable for a business productivity app.
+/// Completely avoids heavy neon glows, replacing them with thin outlines, a clean scale animation,
+/// and an outline mic icon.
+struct PulsingRecordButton: View {
+
+    /// Closure triggered on tap.
+    let action: () -> Void
+
+    /// Controls the subtle pulse animation.
+    @State private var isAnimating: Bool = false
+
+    var body: some View {
+        ZStack {
+            // Precise outer thin outline (pulses slightly)
+            Circle()
+                .stroke(Color.primary.opacity(0.1), lineWidth: 1)
+                .frame(width: 120, height: 120)
+                .scaleEffect(isAnimating ? 1.08 : 0.98)
+                .animation(
+                    .easeInOut(duration: 2.0).repeatForever(autoreverses: true),
+                    value: isAnimating
+                )
+            
+            // Middle thin outline (expanding and fading pulse)
+            Circle()
+                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
+                .frame(width: 100, height: 100)
+                .scaleEffect(isAnimating ? 1.25 : 1.0)
+                .opacity(isAnimating ? 0.0 : 0.8)
+                .animation(
+                    .easeOut(duration: 2.5).repeatForever(autoreverses: false),
+                    value: isAnimating
+                )
+
+            // Inner button body - pure line circle with translucent background
+            Circle()
+                .fill(Color.primary.opacity(0.03))
+                .frame(width: 90, height: 90)
+                .overlay(
+                    Circle()
+                        .stroke(Color.primary.opacity(0.2), lineWidth: 1)
+                )
+                
+            // Inner core indicator - simple line microphone icon
+            Image(systemName: "mic")
+                .font(.system(size: 24, weight: .light))
+                .foregroundColor(.primary)
+                .scaleEffect(isAnimating ? 1.02 : 0.98)
+                .animation(
+                    .easeInOut(duration: 2.0).repeatForever(autoreverses: true),
+                    value: isAnimating
+                )
+        }
+        .frame(width: 120, height: 120)
+        .contentShape(Circle())
+        .onTapGesture {
+            HapticManager.trigger(.recordStart)
+            action()
+        }
+        .onAppear {
+            isAnimating = true
+        }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    ZStack {
+        Color.spaceBlack.ignoresSafeArea()
+        PulsingRecordButton {
+            print("Record tapped")
+        }
+    }
+}

+ 98 - 0
CelestiaTrace/Views/Components/SyncAuthPromptModal.swift

@@ -0,0 +1,98 @@
+import SwiftUI
+
+/// Modal sheet prompting unauthenticated users to register or log in before synchronizing recordings.
+struct SyncAuthPromptModal: View {
+    @Environment(\.dismiss) private var dismiss
+    var onLoginClick: () -> Void
+    
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+            
+            VStack(spacing: 24) {
+                // Header Icon
+                ZStack {
+                    Circle()
+                        .fill(Color.primary.opacity(0.06))
+                        .frame(width: 72, height: 72)
+                    
+                    Image(systemName: "icloud.and.arrow.up")
+                        .font(.system(size: 32, weight: .light))
+                        .foregroundStyle(Color.primary)
+                }
+                .padding(.top, 16)
+                
+                // Title & Subtitle
+                VStack(spacing: 8) {
+                    Text("同步录音需要登录")
+                        .font(.system(size: 18, weight: .bold))
+                        .foregroundStyle(Color.primary)
+                    
+                    Text("请先注册或登录星痕现场记录账号\n数据才能安全同步至云端备份。")
+                        .font(.system(size: 13))
+                        .foregroundStyle(Color.secondary)
+                        .multilineTextAlignment(.center)
+                        .lineSpacing(4)
+                }
+                .padding(.horizontal, 20)
+                
+                // Value propositions
+                VStack(alignment: .leading, spacing: 12) {
+                    featureRow(icon: "lock.shield", text: "端到端云端加密备份")
+                    featureRow(icon: "laptopcomputer.and.iphone", text: "多设备实时同步记录")
+                    featureRow(icon: "mic.fill", text: "专属录音设备绑定与管理")
+                }
+                .padding(16)
+                .background(Color.cardBackground.opacity(0.5))
+                .businessBorder(cornerRadius: 10)
+                .padding(.horizontal, 16)
+                
+                Spacer()
+                
+                // Action Buttons
+                VStack(spacing: 12) {
+                    Button {
+                        dismiss()
+                        onLoginClick()
+                    } label: {
+                        Text("立即注册 / 登录")
+                            .font(.system(size: 15, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                            .frame(maxWidth: .infinity)
+                            .padding(.vertical, 14)
+                            .background(Color.primary)
+                            .cornerRadius(8)
+                    }
+                    
+                    Button {
+                        dismiss()
+                    } label: {
+                        Text("暂不同步")
+                            .font(.system(size: 14, weight: .regular))
+                            .foregroundStyle(Color.secondary)
+                            .padding(.vertical, 8)
+                    }
+                }
+                .padding(.horizontal, 16)
+                .padding(.bottom, 24)
+            }
+        }
+    }
+    
+    private func featureRow(icon: String, text: String) -> some View {
+        HStack(spacing: 12) {
+            Image(systemName: icon)
+                .font(.system(size: 14, weight: .regular))
+                .foregroundStyle(Color.primary)
+                .frame(width: 20)
+            
+            Text(text)
+                .font(.system(size: 13, weight: .regular))
+                .foregroundStyle(Color.primary.opacity(0.85))
+        }
+    }
+}
+
+#Preview {
+    SyncAuthPromptModal(onLoginClick: {})
+}

+ 78 - 0
CelestiaTrace/Views/ContentView.swift

@@ -0,0 +1,78 @@
+import SwiftUI
+import SwiftData
+
+// MARK: - ContentView
+
+/// Root navigation container for the CelestiaTrace app.
+/// Uses a TabView with three tabs: Home, History, and Settings.
+struct ContentView: View {
+
+    /// Currently selected tab.
+    @State private var selectedTab: Tab = .home
+
+    var body: some View {
+        TabView(selection: $selectedTab) {
+            HomeView(onSwitchToHistory: { selectedTab = .history })
+                .tabItem {
+                    Label(Tab.home.title, systemImage: Tab.home.icon)
+                }
+                .tag(Tab.home)
+
+            SessionListView()
+                .tabItem {
+                    Label(Tab.history.title, systemImage: Tab.history.icon)
+                }
+                .tag(Tab.history)
+
+            SettingsView()
+                .tabItem {
+                    Label(Tab.settings.title, systemImage: Tab.settings.icon)
+                }
+                .tag(Tab.settings)
+
+            ProfileView()
+                .tabItem {
+                    Label(Tab.profile.title, systemImage: Tab.profile.icon)
+                }
+                .tag(Tab.profile)
+        }
+        .tint(.celestiaCyan)
+    }
+}
+
+// MARK: - Tab Definition
+
+extension ContentView {
+    /// Defines the available tabs in the app's root navigation.
+    enum Tab: Hashable {
+        case home
+        case history
+        case settings
+        case profile
+
+        var title: String {
+            switch self {
+            case .home: "首页"
+            case .history: "历史记录"
+            case .settings: "设置"
+            case .profile: "个人中心"
+            }
+        }
+
+        var icon: String {
+            switch self {
+            case .home: "house.fill"
+            case .history: "clock.arrow.circlepath"
+            case .settings: "gearshape.fill"
+            case .profile: "person.crop.circle.fill"
+            }
+        }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    ContentView()
+        .modelContainer(for: CelestiaSession.self, inMemory: true)
+}

+ 176 - 0
CelestiaTrace/Views/Detail/ChronoFeedRow.swift

@@ -0,0 +1,176 @@
+import SwiftUI
+
+// MARK: - ChronoFeedRow
+/// A card component for the chronological event feed.
+/// Displays a timeline event with time badge, type icon, content,
+/// and a clean, wireframe bordered layout suitable for business scenarios.
+struct ChronoFeedRow: View {
+    let event: CelestiaTimelineEvent
+    var onTap: (() -> Void)?
+
+    var body: some View {
+        Button {
+            HapticManager.trigger(.tapFeedback)
+            onTap?()
+        } label: {
+            HStack(spacing: 12) {
+                // Left: Time Badge (Minimalist outline)
+                timeBadge
+
+                // Center: Content
+                eventContent
+
+                Spacer(minLength: 4)
+
+                // Right: Chevron
+                Image(systemName: "chevron.right")
+                    .font(.system(size: 9, weight: .light))
+                    .foregroundStyle(Color.secondary.opacity(0.5))
+            }
+            .padding(12)
+            .background(Color.cardBackground.opacity(0.15))
+            .businessBorder(cornerRadius: 8)
+        }
+        .buttonStyle(.plain)
+    }
+
+    // MARK: - Time Badge
+
+    private var timeBadge: some View {
+        Text(event.relativeTimeFormatted)
+            .font(.system(size: 11, weight: .regular, design: .monospaced))
+            .foregroundStyle(Color.primary)
+            .padding(.horizontal, 8)
+            .padding(.vertical, 4)
+            .businessBorder(cornerRadius: 4)
+    }
+
+    // MARK: - Event Content
+
+    private var eventContent: some View {
+        HStack(spacing: 10) {
+            // Event icon (Clean monochrome outline)
+            Image(systemName: eventIconName)
+                .font(.system(size: 12, weight: .light))
+                .foregroundStyle(Color.primary)
+                .frame(width: 24, height: 24)
+                .businessBorder(cornerRadius: 12)
+
+            VStack(alignment: .leading, spacing: 3) {
+                // Type label
+                Text(typeLabel)
+                    .font(.system(size: 9, weight: .medium))
+                    .foregroundStyle(Color.secondary)
+
+                // Content
+                switch event.eventType {
+                case "PHOTO":
+                    photoView
+                case "NOTE", "MARKER":
+                    if let text = event.textContent {
+                        Text(text)
+                            .font(.system(size: 13, weight: .regular))
+                            .foregroundStyle(Color.primary.opacity(0.9))
+                            .lineLimit(2)
+                            .multilineTextAlignment(.leading)
+                    }
+                default:
+                    Text("事件")
+                        .font(.system(size: 13))
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+        }
+    }
+
+    // MARK: - Photo Placeholder
+
+    private var photoPlaceholder: some View {
+        HStack(spacing: 6) {
+            Image(systemName: "photo")
+                .font(.system(size: 10, weight: .light))
+                .foregroundStyle(Color.secondary)
+            Text("照片片段")
+                .font(.system(size: 11))
+                .foregroundStyle(Color.secondary)
+        }
+        .padding(.horizontal, 8)
+        .padding(.vertical, 4)
+        .businessBorder(cornerRadius: 4)
+    }
+
+    @ViewBuilder
+    private var photoView: some View {
+        if let path = event.localFilePath,
+           let uiImage = UIImage(contentsOfFile: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(path).path) {
+            HStack(spacing: 8) {
+                Image(uiImage: uiImage)
+                    .resizable()
+                    .scaledToFill()
+                    .frame(width: 60, height: 45)
+                    .clipShape(RoundedRectangle(cornerRadius: 4))
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 4)
+                            .stroke(Color.lineBorder, lineWidth: 1)
+                    )
+                
+                Text("照片记录")
+                    .font(.system(size: 11))
+                    .foregroundStyle(Color.secondary)
+            }
+        } else {
+            photoPlaceholder
+        }
+    }
+
+    // MARK: - Computed Properties
+
+    private var eventIconName: String {
+        switch event.eventType {
+        case "PHOTO": return "camera"
+        case "NOTE": return "doc.text"
+        case "MARKER": return "flag"
+        case "VOICE": return "waveform"
+        default: return "circle"
+        }
+    }
+
+    private var typeLabel: String {
+        switch event.eventType {
+        case "PHOTO": return "照片"
+        case "NOTE": return "笔记"
+        case "MARKER": return "标记"
+        case "VOICE": return "音频"
+        default: return "事件"
+        }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    VStack(spacing: 12) {
+        let photoEvent: CelestiaTimelineEvent = {
+            let e = CelestiaTimelineEvent(relativeTimeMs: 32000, eventType: "PHOTO")
+            return e
+        }()
+
+        let noteEvent: CelestiaTimelineEvent = {
+            let e = CelestiaTimelineEvent(relativeTimeMs: 65000, eventType: "NOTE")
+            e.textContent = "看到了非常壮观的星空,银河清晰可见"
+            return e
+        }()
+
+        let markerEvent: CelestiaTimelineEvent = {
+            let e = CelestiaTimelineEvent(relativeTimeMs: 120000, eventType: "MARKER")
+            e.textContent = "重要时刻标记"
+            return e
+        }()
+
+        ChronoFeedRow(event: photoEvent)
+        ChronoFeedRow(event: noteEvent)
+        ChronoFeedRow(event: markerEvent)
+    }
+    .padding(16)
+    .background(Color.spaceBlack)
+}

+ 301 - 0
CelestiaTrace/Views/Detail/MultiTrackTimeline.swift

@@ -0,0 +1,301 @@
+import SwiftUI
+
+// MARK: - MultiTrackTimeline
+/// A three-track horizontal timeline visualization showing:
+/// - Track 1: Audio waveform (mock sine wave outline)
+/// - Track 2: Photo markers (camera outlines at event positions)
+/// - Track 3: Note/marker outline icons at event positions
+/// Includes a time ruler and a central playhead cursor.
+/// Fully redesigned with a minimalist business line-drawn style.
+struct MultiTrackTimeline: View {
+    let events: [CelestiaTimelineEvent]
+    @Binding var currentTimeMs: Double
+    let totalDurationMs: Double
+    var silentRanges: [SilenceRange] = []
+
+    /// Scale: points per second
+    private let pointsPerSecond: CGFloat = 2.5
+    /// Minimum timeline width
+    private let minimumWidth: CGFloat = 400
+
+    private var timelineWidth: CGFloat {
+        max(minimumWidth, CGFloat(totalDurationMs / 1000.0) * pointsPerSecond + 60)
+    }
+
+    var body: some View {
+        VStack(spacing: 0) {
+            ScrollViewReader { scrollProxy in
+                ScrollView(.horizontal, showsIndicators: false) {
+                    ZStack(alignment: .topLeading) {
+                        // Background
+                        RoundedRectangle(cornerRadius: 10)
+                            .fill(Color.cardBackground.opacity(0.15))
+
+                        VStack(spacing: 0) {
+                            // Time Ruler
+                            timeRuler
+                                .frame(height: 26)
+
+                            // Track 1: Audio Waveform
+                            audioTrack
+                                .frame(height: 38)
+
+                            trackDivider
+
+                            // Track 2: Photo Markers
+                            photoTrack
+                                .frame(height: 28)
+
+                            trackDivider
+
+                            // Track 3: Note Markers
+                            noteTrack
+                                .frame(height: 28)
+                        }
+
+                        // Playhead Cursor
+                        playhead
+                            .id("playhead")
+                    }
+                    .frame(width: timelineWidth)
+                }
+                .clipShape(RoundedRectangle(cornerRadius: 10))
+                .businessBorder(cornerRadius: 10)
+                .onChange(of: currentTimeMs) { _, _ in
+                    withAnimation(.easeOut(duration: 0.2)) {
+                        scrollProxy.scrollTo("playhead", anchor: .center)
+                    }
+                }
+            }
+
+            // Track labels
+            trackLabels
+                .padding(.top, 8)
+        }
+    }
+
+    // MARK: - Time Ruler
+
+    private var timeRuler: some View {
+        Canvas { context, size in
+            let totalSeconds = totalDurationMs / 1000.0
+            let markerInterval: Double = totalSeconds > 300 ? 60 : (totalSeconds > 60 ? 10 : 5)
+
+            var t: Double = 0
+            while t <= totalSeconds {
+                let x = xPosition(for: t * 1000)
+
+                // Tick mark
+                let tickPath = Path { path in
+                    path.move(to: CGPoint(x: x, y: size.height - 6))
+                    path.addLine(to: CGPoint(x: x, y: size.height))
+                }
+                context.stroke(tickPath, with: .color(Color.primary.opacity(0.2)), lineWidth: 1)
+
+                // Time label
+                let minutes = Int(t) / 60
+                let seconds = Int(t) % 60
+                let label = String(format: "%d:%02d", minutes, seconds)
+
+                let text = Text(label)
+                    .font(.system(size: 8, weight: .regular, design: .monospaced))
+                    .foregroundStyle(Color.secondary.opacity(0.8))
+
+                context.draw(
+                    context.resolve(text),
+                    at: CGPoint(x: x, y: size.height - 14),
+                    anchor: .center
+                )
+
+                t += markerInterval
+            }
+        }
+    }
+
+    // MARK: - Audio Track
+
+    private var audioTrack: some View {
+        Canvas { context, size in
+            let sampleCount = Int(size.width)
+            guard sampleCount > 0 else { return }
+            
+            let waveformPath = Path { path in
+                for i in 0..<sampleCount {
+                    let x = CGFloat(i)
+                    let progress = Double(i) / Double(sampleCount)
+                    let time = progress * (totalDurationMs / 1000.0)
+
+                    let isInSilence = silentRanges.contains(where: { time >= $0.start && time < $0.end })
+                    let amplitude: Double
+                    if isInSilence {
+                        amplitude = 0.02
+                    } else {
+                        amplitude = abs(
+                            sin(time * 3.0) * 0.45 +
+                            sin(time * 7.3) * 0.2 +
+                            sin(time * 13.7) * 0.1 +
+                            sin(time * 0.8) * 0.15
+                        )
+                    }
+
+                    let height = amplitude * Double(size.height * 0.7)
+                    let centerY = size.height / 2
+
+                    if i == 0 {
+                        path.move(to: CGPoint(x: x, y: centerY - height / 2))
+                    } else {
+                        path.addLine(to: CGPoint(x: x, y: centerY - height / 2))
+                    }
+                }
+
+                // Mirror back
+                for i in stride(from: sampleCount - 1, through: 0, by: -1) {
+                    let x = CGFloat(i)
+                    let progress = Double(i) / Double(sampleCount)
+                    let time = progress * (totalDurationMs / 1000.0)
+
+                    let isInSilence = silentRanges.contains(where: { time >= $0.start && time < $0.end })
+                    let amplitude: Double
+                    if isInSilence {
+                        amplitude = 0.02
+                    } else {
+                        amplitude = abs(
+                            sin(time * 3.0) * 0.45 +
+                            sin(time * 7.3) * 0.2 +
+                            sin(time * 13.7) * 0.1 +
+                            sin(time * 0.8) * 0.15
+                        )
+                    }
+
+                    let height = amplitude * Double(size.height * 0.7)
+                    let centerY = size.height / 2
+
+                    path.addLine(to: CGPoint(x: x, y: centerY + height / 2))
+                }
+                path.closeSubpath()
+            }
+
+            // Fill with a very subtle neutral monochrome shade
+            context.fill(waveformPath, with: .color(Color.primary.opacity(0.08)))
+            
+            // Draw clean outline
+            context.stroke(waveformPath, with: .color(Color.primary.opacity(0.35)), lineWidth: 0.75)
+
+            // Center line
+            let centerLine = Path { path in
+                path.move(to: CGPoint(x: 0, y: size.height / 2))
+                path.addLine(to: CGPoint(x: size.width, y: size.height / 2))
+            }
+            context.stroke(centerLine, with: .color(Color.primary.opacity(0.1)), lineWidth: 0.5)
+        }
+    }
+
+    // MARK: - Photo Track
+
+    private var photoTrack: some View {
+        ZStack(alignment: .leading) {
+            Color.clear
+
+            ForEach(photoEvents) { event in
+                Image(systemName: "camera")
+                    .font(.system(size: 9))
+                    .foregroundStyle(Color.primary.opacity(0.8))
+                    .frame(width: 20, height: 20)
+                    .background(Color.cardBackground.opacity(0.6))
+                    .businessBorder(cornerRadius: 10)
+                    .offset(x: xPosition(for: Double(event.relativeTimeMs)) - 10)
+            }
+        }
+    }
+
+    // MARK: - Note Track
+
+    private var noteTrack: some View {
+        ZStack(alignment: .leading) {
+            Color.clear
+
+            ForEach(noteEvents) { event in
+                let iconName = event.eventType == "MARKER" ? "exclamationmark.triangle" : "doc.text"
+                Image(systemName: iconName)
+                    .font(.system(size: 9))
+                    .foregroundStyle(Color.primary.opacity(0.8))
+                    .frame(width: 20, height: 20)
+                    .background(Color.cardBackground.opacity(0.6))
+                    .businessBorder(cornerRadius: 10)
+                    .offset(x: xPosition(for: Double(event.relativeTimeMs)) - 10)
+            }
+        }
+    }
+
+    // MARK: - Playhead
+
+    private var playhead: some View {
+        let x = xPosition(for: currentTimeMs)
+        return Rectangle()
+            .fill(Color.primary) // Dynamic primary color for playhead line
+            .frame(width: 1.0)
+            .offset(x: x)
+            .animation(.easeOut(duration: 0.15), value: currentTimeMs)
+    }
+
+    // MARK: - Track Divider
+
+    private var trackDivider: some View {
+        Rectangle()
+            .fill(Color.lineBorder)
+            .frame(height: 1)
+    }
+
+    // MARK: - Track Labels
+
+    private var trackLabels: some View {
+        HStack(spacing: 24) {
+            trackLabel(icon: "waveform", text: "音频")
+            trackLabel(icon: "camera", text: "图片")
+            trackLabel(icon: "doc.text", text: "笔记")
+        }
+    }
+
+    private func trackLabel(icon: String, text: String) -> some View {
+        HStack(spacing: 4) {
+            Image(systemName: icon)
+                .font(.system(size: 9, weight: .light))
+            Text(text)
+                .font(.system(size: 9, weight: .regular))
+        }
+        .foregroundStyle(Color.secondary)
+    }
+
+    // MARK: - Helpers
+
+    private func xPosition(for timeMs: Double) -> CGFloat {
+        guard totalDurationMs > 0 else { return 30 }
+        let padding: CGFloat = 30
+        let usableWidth = timelineWidth - padding * 2
+        let ratio = timeMs / totalDurationMs
+        return padding + usableWidth * CGFloat(ratio)
+    }
+
+    private var photoEvents: [CelestiaTimelineEvent] {
+        events.filter { $0.eventType == "PHOTO" }
+    }
+
+    private var noteEvents: [CelestiaTimelineEvent] {
+        events.filter { $0.eventType == "NOTE" || $0.eventType == "MARKER" }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    VStack {
+        MultiTrackTimeline(
+            events: [],
+            currentTimeMs: .constant(30000),
+            totalDurationMs: 120000
+        )
+        .frame(height: 140)
+        .padding()
+    }
+    .background(Color.spaceBlack)
+}

+ 399 - 0
CelestiaTrace/Views/Detail/SessionDetailView.swift

@@ -0,0 +1,399 @@
+import SwiftUI
+import SwiftData
+
+// MARK: - SessionDetailView
+/// The session detail & playback screen.
+/// Shows session info, multi-track timeline, playback controls,
+/// and a chronological feed of all events.
+/// Redesigned to use a minimalist wireframe business style.
+struct SessionDetailView: View {
+    @Environment(\.modelContext) private var modelContext
+
+    let session: CelestiaSession
+
+    @State private var playbackVM = PlaybackViewModel()
+    @State private var navigateToRecording = false
+    @State private var detector = RecordingEnvironmentDetector()
+    @State private var showPrepAlert = false
+    @State private var activeWarningMsg = ""
+
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+
+            ScrollView {
+                VStack(spacing: 20) {
+                    // Section 1: Session Info Card
+                    sessionInfoCard
+                        .padding(.horizontal, 16)
+                        .padding(.top, 12)
+
+                    // Section 2: Multi-Track Timeline
+                    timelineSection
+                        .padding(.horizontal, 16)
+
+                    // Section 3: Playback Controls
+                    playbackControls
+                        .padding(.horizontal, 16)
+
+                    // Section 4: Chrono Feed
+                    chronoFeedSection
+                        .padding(.horizontal, 16)
+                        .padding(.bottom, 32)
+                }
+            }
+        }
+        .navigationBarTitleDisplayMode(.inline)
+        .toolbar {
+            ToolbarItem(placement: .principal) {
+                Text(session.title)
+                    .font(.system(size: 14, weight: .semibold))
+                    .foregroundStyle(Color.primary)
+                    .lineLimit(1)
+            }
+            ToolbarItem(placement: .topBarTrailing) {
+                Button {
+                    detector.checkEnvironment()
+                    if let firstWarning = detector.activeWarnings.first {
+                        activeWarningMsg = firstWarning
+                        showPrepAlert = true
+                    } else {
+                        navigateToRecording = true
+                    }
+                } label: {
+                    HStack(spacing: 4) {
+                        Image(systemName: "plus")
+                            .font(.system(size: 11, weight: .medium))
+                        Text("续录")
+                            .font(.system(size: 12, weight: .regular))
+                    }
+                    .foregroundStyle(Color.primary)
+                    .padding(.horizontal, 10)
+                    .padding(.vertical, 4)
+                    .background(Color.primary.opacity(0.02))
+                    .businessBorder(cornerRadius: 6)
+                }
+            }
+        }
+        .alert("录音环境提醒", isPresented: $showPrepAlert) {
+            Button("继续录制", role: .none) {
+                navigateToRecording = true
+            }
+            Button("取消", role: .cancel) {}
+        } message: {
+            Text(activeWarningMsg + "\n\n另外请手动确认:\n1. 手机侧边静音开关已拨至红色(静音状态)\n2. 已关闭可能在此期间响起的系统闹钟")
+        }
+        .fullScreenCover(isPresented: $navigateToRecording) {
+            ActiveRecordingView(session: session) {
+                playbackVM.totalDurationMs = Double(session.durationMs)
+                playbackVM.preparePlayer(path: session.localAudioPath, durationMs: Double(session.durationMs))
+                playbackVM.analyzeSilence(audioURL: session.localAudioPath.map { URL(fileURLWithPath: $0) })
+            }
+        }
+        .onAppear {
+            playbackVM.totalDurationMs = Double(session.durationMs)
+            playbackVM.preparePlayer(path: session.localAudioPath, durationMs: Double(session.durationMs))
+            playbackVM.analyzeSilence(audioURL: session.localAudioPath.map { URL(fileURLWithPath: $0) })
+        }
+    }
+
+    // MARK: - Session Info Card
+
+    private var sessionInfoCard: some View {
+        VStack(spacing: 14) {
+            // Date range
+            HStack {
+                VStack(alignment: .leading, spacing: 4) {
+                    Label {
+                        Text("开始")
+                            .font(.system(size: 10, weight: .medium))
+                            .foregroundStyle(Color.secondary)
+                    } icon: {
+                        Image(systemName: "play.circle")
+                            .font(.system(size: 11))
+                            .foregroundStyle(Color.secondary)
+                    }
+
+                    Text(session.startTime.formatted(.dateTime.month().day().hour().minute()))
+                        .font(.system(size: 13, weight: .medium))
+                        .foregroundStyle(Color.primary)
+                }
+
+                Spacer()
+
+                VStack(alignment: .trailing, spacing: 4) {
+                    Label {
+                        Text("结束")
+                            .font(.system(size: 10, weight: .medium))
+                            .foregroundStyle(Color.secondary)
+                    } icon: {
+                        Image(systemName: "stop.circle")
+                            .font(.system(size: 11))
+                            .foregroundStyle(Color.secondary)
+                    }
+
+                    Text(session.endTime?.formatted(.dateTime.month().day().hour().minute()) ?? "进行中")
+                        .font(.system(size: 13, weight: .medium))
+                        .foregroundStyle(Color.primary)
+                }
+            }
+
+            Divider()
+                .background(Color.lineBorder)
+
+            // Stats row
+            HStack(spacing: 0) {
+                infoStat(
+                    icon: "timer",
+                    label: "总时长",
+                    value: session.durationFormatted
+                )
+
+                infoStat(
+                    icon: "camera",
+                    label: "图片",
+                    value: "\(session.photoCount)"
+                )
+
+                infoStat(
+                    icon: "doc.text",
+                    label: "笔记",
+                    value: "\(session.noteCount)"
+                )
+
+                // Sync status
+                VStack(spacing: 4) {
+                    Image(systemName: session.isSynced ? "cloud" : "cloud.dashed")
+                        .font(.system(size: 14, weight: .light))
+                        .foregroundStyle(Color.secondary)
+
+                    Text(session.isSynced ? "已同步" : "未同步")
+                        .font(.system(size: 10, weight: .regular))
+                        .foregroundStyle(Color.secondary)
+                }
+                .frame(maxWidth: .infinity)
+            }
+        }
+        .padding(14)
+        .background(Color.cardBackground.opacity(0.15))
+        .businessBorder(cornerRadius: 10)
+    }
+
+    private func infoStat(icon: String, label: String, value: String) -> some View {
+        VStack(spacing: 4) {
+            Image(systemName: icon)
+                .font(.system(size: 14, weight: .light))
+                .foregroundStyle(Color.secondary)
+
+            Text(value)
+                .font(.system(size: 13, weight: .medium, design: .monospaced))
+                .foregroundStyle(Color.primary)
+
+            Text(label)
+                .font(.system(size: 9, weight: .regular))
+                .foregroundStyle(Color.secondary.opacity(0.8))
+        }
+        .frame(maxWidth: .infinity)
+    }
+
+    // MARK: - Timeline Section
+
+    private var timelineSection: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            sectionHeader(icon: "waveform", title: "三轨时间线")
+
+            MultiTrackTimeline(
+                events: session.events,
+                currentTimeMs: Binding(
+                    get: { playbackVM.currentPlaybackTimeMs },
+                    set: { playbackVM.seekTo(timeMs: $0) }
+                ),
+                totalDurationMs: Double(session.durationMs),
+                silentRanges: playbackVM.silentRanges
+            )
+            .frame(height: 140)
+        }
+    }
+
+    // MARK: - Playback Controls
+
+    private var playbackControls: some View {
+        VStack(spacing: 12) {
+            // Time display
+            HStack {
+                Text(playbackVM.currentTimeFormatted)
+                    .font(.system(size: 12, weight: .regular, design: .monospaced))
+                    .foregroundStyle(Color.primary)
+
+                Spacer()
+
+                Text(totalTimeFormatted)
+                    .font(.system(size: 12, weight: .regular, design: .monospaced))
+                    .foregroundStyle(Color.secondary)
+            }
+
+            // Seek slider
+            Slider(
+                value: Binding(
+                    get: { playbackVM.progress },
+                    set: { newValue in
+                        playbackVM.seekTo(timeMs: newValue * Double(session.durationMs))
+                    }
+                ),
+                in: 0...1
+            )
+            .tint(Color.primary)
+
+            // Play/Pause button
+            HStack(spacing: 36) {
+                // Rewind 10s
+                Button {
+                    let target = max(0, playbackVM.currentPlaybackTimeMs - 10_000)
+                    playbackVM.seekTo(timeMs: target)
+                } label: {
+                    Image(systemName: "gobackward.10")
+                        .font(.system(size: 18, weight: .light))
+                        .foregroundStyle(Color.secondary)
+                }
+
+                // Play/Pause (High-contrast minimalist button)
+                Button {
+                    playbackVM.togglePlayback()
+                    HapticManager.trigger(.tapFeedback)
+                } label: {
+                    ZStack {
+                        Circle()
+                            .fill(Color.primary)
+                            .frame(width: 48, height: 48)
+
+                        Image(systemName: playbackVM.isPlaying ? "pause.fill" : "play.fill")
+                            .font(.system(size: 16, weight: .bold))
+                            .foregroundStyle(Color.spaceBlack)
+                    }
+                }
+
+                // Forward 10s
+                Button {
+                    let target = min(Double(session.durationMs), playbackVM.currentPlaybackTimeMs + 10_000)
+                    playbackVM.seekTo(timeMs: target)
+                } label: {
+                    Image(systemName: "goforward.10")
+                        .font(.system(size: 18, weight: .light))
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+            .padding(.top, 4)
+            
+            Divider()
+                .background(Color.lineBorder)
+                .padding(.vertical, 4)
+            
+            HStack {
+                Label {
+                    Text("智能跳过静音")
+                        .font(.system(size: 12, weight: .medium))
+                        .foregroundStyle(Color.primary)
+                } icon: {
+                    Image(systemName: playbackVM.isSilenceSkipEnabled ? "waveform.badge.minus" : "waveform")
+                        .font(.system(size: 13))
+                        .foregroundStyle(playbackVM.isSilenceSkipEnabled ? Color.primary : Color.secondary)
+                }
+                
+                Spacer()
+                
+                if playbackVM.isAnalyzingSilence {
+                    ProgressView()
+                        .scaleEffect(0.7)
+                        .frame(width: 16, height: 16)
+                } else if !playbackVM.silentRanges.isEmpty {
+                    Text("检测到 \(playbackVM.silentRanges.count) 处静音")
+                        .font(.system(size: 11))
+                        .foregroundStyle(Color.secondary)
+                }
+                
+                Toggle("", isOn: Binding(
+                    get: { playbackVM.isSilenceSkipEnabled },
+                    set: { playbackVM.isSilenceSkipEnabled = $0 }
+                ))
+                .toggleStyle(SwitchToggleStyle(tint: Color.primary))
+                .labelsHidden()
+                .scaleEffect(0.8)
+            }
+        }
+        .padding(14)
+        .background(Color.cardBackground.opacity(0.15))
+        .businessBorder(cornerRadius: 10)
+    }
+
+    // MARK: - Chrono Feed
+
+    private var chronoFeedSection: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            sectionHeader(icon: "list.dash", title: "时序事件列表")
+
+            let sorted = playbackVM.sortedEvents(from: session)
+
+            if sorted.isEmpty {
+                HStack {
+                    Spacer()
+                    VStack(spacing: 8) {
+                        Image(systemName: "tray")
+                            .font(.system(size: 24, weight: .light))
+                            .foregroundStyle(Color.secondary.opacity(0.3))
+                        Text("暂无会话事件")
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.secondary.opacity(0.5))
+                    }
+                    .padding(.vertical, 24)
+                    Spacer()
+                }
+            } else {
+                LazyVStack(spacing: 8) {
+                    ForEach(sorted) { event in
+                        ChronoFeedRow(event: event) {
+                            playbackVM.seekTo(timeMs: event.relativeTimeMs)
+                            HapticManager.trigger(.tapFeedback)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // MARK: - Helpers
+
+    private func sectionHeader(icon: String, title: String) -> some View {
+        HStack(spacing: 6) {
+            Image(systemName: icon)
+                .font(.system(size: 12, weight: .light))
+                .foregroundStyle(Color.secondary)
+            Text(title)
+                .font(.system(size: 13, weight: .semibold))
+                .foregroundStyle(Color.primary.opacity(0.8))
+        }
+    }
+
+    private var totalTimeFormatted: String {
+        let totalSeconds = Int(session.durationMs / 1000)
+        let hours = totalSeconds / 3600
+        let minutes = (totalSeconds % 3600) / 60
+        let seconds = totalSeconds % 60
+        if hours > 0 {
+            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
+        }
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    NavigationStack {
+        SessionDetailView(session: {
+            let s = CelestiaSession(title: "Preview 现场记录 · 6.24 14:30")
+            s.endTime = Date().addingTimeInterval(3600)
+            return s
+        }())
+    }
+    .modelContainer(for: CelestiaSession.self, inMemory: true)
+}

+ 237 - 0
CelestiaTrace/Views/History/SessionListView.swift

@@ -0,0 +1,237 @@
+import SwiftUI
+import SwiftData
+
+// MARK: - SessionListView
+/// The session archive screen showing all recorded sessions
+/// in a searchable, card-style list with sync status indicators.
+/// Redesigned with a minimalist wireframe business style.
+struct SessionListView: View {
+    @Environment(\.modelContext) private var modelContext
+    @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
+
+    @State private var searchText = ""
+    @State private var showSyncAuthPrompt = false
+    @State private var showAuthModal = false
+
+    var body: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+
+                if filteredSessions.isEmpty {
+                    emptyState
+                } else {
+                    sessionList
+                }
+            }
+            .navigationTitle("会话档案")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button {
+                        AuthManager.shared.syncRecordings {
+                            showSyncAuthPrompt = true
+                        } onComplete: { _ in }
+                    } label: {
+                        HStack(spacing: 4) {
+                            Image(systemName: "icloud.and.arrow.up")
+                                .font(.system(size: 13))
+                            Text("同步")
+                                .font(.system(size: 13, weight: .medium))
+                        }
+                        .foregroundStyle(Color.primary)
+                    }
+                }
+            }
+            .searchable(
+                text: $searchText,
+                placement: .navigationBarDrawer(displayMode: .always),
+                prompt: "搜索会话或笔记..."
+            )
+            .sheet(isPresented: $showSyncAuthPrompt) {
+                SyncAuthPromptModal(onLoginClick: {
+                    showAuthModal = true
+                })
+            }
+            .sheet(isPresented: $showAuthModal) {
+                AuthModalView()
+            }
+        }
+    }
+
+    // MARK: - Filtered Sessions
+
+    private var filteredSessions: [CelestiaSession] {
+        guard !searchText.isEmpty else { return sessions }
+        let query = searchText.lowercased()
+        return sessions.filter { session in
+            if session.title.lowercased().contains(query) { return true }
+            return session.events.contains { event in
+                event.textContent?.lowercased().contains(query) == true
+            }
+        }
+    }
+
+    // MARK: - Session List
+
+    private var sessionList: some View {
+        ScrollView {
+            LazyVStack(spacing: 12) {
+                // Stats header
+                statsHeader
+                    .padding(.horizontal, 16)
+                    .padding(.top, 12)
+
+                ForEach(filteredSessions) { session in
+                    NavigationLink(destination: SessionDetailView(session: session)) {
+                        SessionRowCard(session: session)
+                    }
+                    .buttonStyle(.plain)
+                    .padding(.horizontal, 16)
+                }
+            }
+            .padding(.bottom, 24)
+        }
+    }
+
+    // MARK: - Stats Header
+
+    private var statsHeader: some View {
+        HStack(spacing: 20) {
+            statBadge(
+                value: "\(filteredSessions.count)",
+                label: "总会话",
+                icon: "waveform"
+            )
+
+            statBadge(
+                value: "\(filteredSessions.reduce(0) { $0 + $1.photoCount })",
+                label: "已获图片",
+                icon: "camera"
+            )
+
+            statBadge(
+                value: "\(filteredSessions.filter { $0.isSynced }.count)",
+                label: "已同步",
+                icon: "icloud"
+            )
+        }
+        .padding(.vertical, 14)
+        .background(Color.cardBackground.opacity(0.3))
+        .businessBorder(cornerRadius: 10)
+    }
+
+    private func statBadge(value: String, label: String, icon: String) -> some View {
+        VStack(spacing: 4) {
+            Image(systemName: icon)
+                .font(.system(size: 13, weight: .light))
+                .foregroundStyle(Color.secondary)
+
+            Text(value)
+                .font(.system(size: 16, weight: .medium, design: .monospaced))
+                .foregroundStyle(Color.primary)
+
+            Text(label)
+                .font(.system(size: 10, weight: .regular))
+                .foregroundStyle(Color.secondary.opacity(0.8))
+        }
+        .frame(maxWidth: .infinity)
+    }
+
+    // MARK: - Empty State
+
+    private var emptyState: some View {
+        VStack(spacing: 16) {
+            Image(systemName: "square.dashed")
+                .font(.system(size: 36))
+                .foregroundStyle(Color.secondary.opacity(0.4))
+
+            Text("暂无记录")
+                .font(.system(size: 14, weight: .medium))
+                .foregroundStyle(Color.primary)
+
+            if !searchText.isEmpty {
+                Text("未找到与「\(searchText)」匹配的会话")
+                    .font(.system(size: 12))
+                    .foregroundStyle(Color.secondary)
+            } else {
+                Text("启动全局录音来保存你的第一条记录")
+                    .font(.system(size: 12))
+                    .foregroundStyle(Color.secondary.opacity(0.7))
+            }
+        }
+        .padding(.bottom, 60)
+    }
+}
+
+// MARK: - Session Row Card
+
+private struct SessionRowCard: View {
+    let session: CelestiaSession
+
+    var body: some View {
+        HStack(spacing: 12) {
+            // Sync status indicator (plain circular dot)
+            Circle()
+                .fill(session.isSynced ? Color.secondary.opacity(0.4) : Color.recordingRed.opacity(0.6))
+                .frame(width: 6, height: 6)
+
+            VStack(alignment: .leading, spacing: 4) {
+                Text(session.title)
+                    .font(.system(size: 14, weight: .medium))
+                    .foregroundStyle(Color.primary)
+                    .lineLimit(1)
+
+                HStack(spacing: 6) {
+                    Text(session.startTime.formatted(.dateTime.month().day().hour().minute()))
+                        .font(.system(size: 11))
+                        .foregroundStyle(Color.secondary)
+
+                    Text("·")
+                        .foregroundStyle(Color.secondary.opacity(0.4))
+
+                    Text(session.durationFormatted)
+                        .font(.system(size: 11, weight: .regular, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+
+            Spacer()
+
+            // Count badges (plain line style)
+            HStack(spacing: 8) {
+                countBadge(icon: "camera", count: session.photoCount)
+                countBadge(icon: "doc.text", count: session.noteCount)
+            }
+            .padding(.trailing, 4)
+
+            // Chevron
+            Image(systemName: "chevron.right")
+                .font(.system(size: 10, weight: .light))
+                .foregroundStyle(Color.secondary.opacity(0.5))
+        }
+        .padding(14)
+        .background(Color.cardBackground.opacity(0.2))
+        .businessBorder(cornerRadius: 8)
+    }
+
+    private func countBadge(icon: String, count: Int) -> some View {
+        HStack(spacing: 3) {
+            Image(systemName: icon)
+                .font(.system(size: 10, weight: .light))
+            Text("\(count)")
+                .font(.system(size: 11, weight: .regular, design: .monospaced))
+        }
+        .foregroundStyle(Color.secondary)
+        .padding(.horizontal, 6)
+        .padding(.vertical, 2)
+        .businessBorder(cornerRadius: 4)
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    SessionListView()
+        .modelContainer(for: CelestiaSession.self, inMemory: true)
+}

+ 303 - 0
CelestiaTrace/Views/Home/HomeView.swift

@@ -0,0 +1,303 @@
+import SwiftUI
+import SwiftData
+
+// MARK: - HomeView
+/// The main launch screen of CelestiaTrace.
+/// Features a minimalist, line-drawn aesthetic with a central record button,
+/// bordered session cards, and a subtle technical grid background.
+struct HomeView: View {
+    @Environment(\.modelContext) private var modelContext
+    @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
+
+    @State private var navigateToRecording = false
+    @State private var activeSession: CelestiaSession?
+    @State private var selectedSessionForDetail: CelestiaSession?
+    @State private var detector = RecordingEnvironmentDetector()
+    @State private var dismissedWarnings: Set<String> = []
+
+    /// Callback to switch to the History tab from the parent TabView.
+    var onSwitchToHistory: (() -> Void)?
+
+    var body: some View {
+        NavigationStack {
+            ZStack {
+                // Dynamic clean background
+                Color.spaceBlack
+                    .ignoresSafeArea()
+
+                // Subtle technical grid background
+                businessGrid
+
+                // Main content
+                VStack(spacing: 0) {
+                    topBar
+                        .padding(.top, 16)
+
+                    if let firstWarning = detector.activeWarnings.first(where: { !dismissedWarnings.contains($0) }) {
+                        DiscreetWarningBanner(warning: firstWarning) {
+                            withAnimation {
+                                _ = dismissedWarnings.insert(firstWarning)
+                            }
+                        }
+                        .padding(.top, 12)
+                    }
+
+                    Spacer()
+
+                    recordSection
+
+                    Spacer()
+
+                    recentSessionsSection
+                        .padding(.bottom, 16)
+
+                    historyLink
+                        .padding(.bottom, 24)
+                }
+                .padding(.horizontal, 24)
+            }
+            .navigationBarHidden(true)
+            .onAppear {
+                detector.checkEnvironment()
+            }
+            .fullScreenCover(isPresented: $navigateToRecording) {
+                if let session = activeSession {
+                    ActiveRecordingView(session: session) {
+                        selectedSessionForDetail = session
+                    }
+                }
+            }
+            .navigationDestination(item: $selectedSessionForDetail) { session in
+                SessionDetailView(session: session)
+            }
+        }
+    }
+
+    // MARK: - Top Bar
+
+    private var topBar: some View {
+        HStack(alignment: .top) {
+            VStack(alignment: .leading, spacing: 4) {
+                Text("星痕现场记录")
+                    .font(.system(size: 11, weight: .bold, design: .monospaced))
+                    .foregroundStyle(Color.primary.opacity(0.8))
+                    .tracking(3)
+
+                Text(currentDateFormatted)
+                    .font(.system(size: 13, weight: .regular))
+                    .foregroundStyle(Color.secondary)
+            }
+            Spacer()
+
+            // Status indicator
+            HStack(spacing: 6) {
+                Circle()
+                    .fill(Color.primary.opacity(0.7))
+                    .frame(width: 5, height: 5)
+                Text("就绪")
+                    .font(.system(size: 9, weight: .semibold, design: .monospaced))
+                    .foregroundStyle(Color.secondary)
+            }
+            .padding(.horizontal, 8)
+            .padding(.vertical, 4)
+            .businessBorder(cornerRadius: 4)
+        }
+    }
+
+    // MARK: - Record Section
+
+    private var recordSection: some View {
+        VStack(spacing: 24) {
+            PulsingRecordButton {
+                createSessionAndNavigate()
+            }
+
+            VStack(spacing: 6) {
+                Text("新建现场记录")
+                    .font(.system(size: 16, weight: .medium))
+                    .foregroundStyle(Color.primary)
+
+                Text("点击开启全新现场录音")
+                    .font(.system(size: 9, weight: .medium, design: .monospaced))
+                    .foregroundStyle(Color.secondary.opacity(0.7))
+                    .tracking(2)
+            }
+        }
+    }
+
+    // MARK: - Recent Sessions
+
+    private var recentSessionsSection: some View {
+        VStack(alignment: .leading, spacing: 12) {
+            HStack {
+                Image(systemName: "list.dash")
+                    .font(.system(size: 12))
+                    .foregroundStyle(Color.secondary)
+                Text("最近记录")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(Color.primary.opacity(0.8))
+            }
+
+            if sessions.isEmpty {
+                emptyRecentState
+            } else {
+                ScrollView(.horizontal, showsIndicators: false) {
+                    HStack(spacing: 12) {
+                        ForEach(sessions.prefix(3)) { session in
+                            Button {
+                                selectedSessionForDetail = session
+                            } label: {
+                                RecentSessionCard(session: session)
+                            }
+                            .buttonStyle(.plain)
+                        }
+                    }
+                    .padding(.vertical, 2)
+                }
+            }
+        }
+    }
+
+    private var emptyRecentState: some View {
+        HStack {
+            Spacer()
+            VStack(spacing: 8) {
+                Image(systemName: "plus.square.dashed")
+                    .font(.system(size: 20))
+                    .foregroundStyle(Color.secondary.opacity(0.4))
+                Text("暂无记录")
+                    .font(.system(size: 12))
+                    .foregroundStyle(Color.secondary.opacity(0.5))
+            }
+            .padding(.vertical, 32)
+            Spacer()
+        }
+        .background(Color.cardBackground.opacity(0.3))
+        .businessBorder(cornerRadius: 8)
+    }
+
+    // MARK: - History Link
+
+    private var historyLink: some View {
+        Button {
+            onSwitchToHistory?()
+        } label: {
+            HStack(spacing: 6) {
+                Text("查看历史列表")
+                    .font(.system(size: 13, weight: .regular))
+                Image(systemName: "chevron.right")
+                    .font(.system(size: 10))
+            }
+            .foregroundStyle(Color.primary.opacity(0.7))
+            .padding(.horizontal, 14)
+            .padding(.vertical, 8)
+            .businessBorder(cornerRadius: 6)
+        }
+    }
+
+    // MARK: - Technical Grid Background
+
+    private var businessGrid: some View {
+        Canvas { context, size in
+            let step: CGFloat = 40
+            let cols = Int(size.width / step)
+            let rows = Int(size.height / step)
+            
+            for col in 0...cols {
+                let x = CGFloat(col) * step
+                var path = Path()
+                path.move(to: CGPoint(x: x, y: 0))
+                path.addLine(to: CGPoint(x: x, y: size.height))
+                context.stroke(path, with: .color(Color.primary.opacity(0.015)), lineWidth: 0.5)
+            }
+            for row in 0...rows {
+                let y = CGFloat(row) * step
+                var path = Path()
+                path.move(to: CGPoint(x: 0, y: y))
+                path.addLine(to: CGPoint(x: size.width, y: y))
+                context.stroke(path, with: .color(Color.primary.opacity(0.015)), lineWidth: 0.5)
+            }
+        }
+        .ignoresSafeArea()
+        .allowsHitTesting(false)
+    }
+
+    // MARK: - Actions
+
+    private func createSessionAndNavigate() {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyy.MM.dd HH:mm"
+        let title = "现场记录 · \(formatter.string(from: Date()))"
+
+        let session = CelestiaSession(title: title)
+        modelContext.insert(session)
+        try? modelContext.save()
+
+        activeSession = session
+        HapticManager.trigger(.recordStart)
+        navigateToRecording = true
+    }
+
+    // MARK: - Helpers
+
+    private var currentDateFormatted: String {
+        let formatter = DateFormatter()
+        formatter.locale = Locale(identifier: "zh_Hans")
+        formatter.dateFormat = "yyyy年M月d日 EEEE"
+        return formatter.string(from: Date())
+    }
+}
+
+// MARK: - Recent Session Card
+
+private struct RecentSessionCard: View {
+    let session: CelestiaSession
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text(session.title)
+                .font(.system(size: 13, weight: .medium))
+                .foregroundStyle(Color.primary)
+                .lineLimit(1)
+
+            HStack(spacing: 4) {
+                Image(systemName: "waveform")
+                    .font(.system(size: 9))
+                    .foregroundStyle(Color.secondary)
+                Text(session.durationFormatted)
+                    .font(.system(size: 11, weight: .regular, design: .monospaced))
+                    .foregroundStyle(Color.secondary)
+            }
+
+            HStack(spacing: 12) {
+                HStack(spacing: 3) {
+                    Image(systemName: "camera")
+                        .font(.system(size: 9))
+                    Text("\(session.photoCount)")
+                        .font(.system(size: 10, design: .monospaced))
+                }
+                .foregroundStyle(Color.secondary)
+
+                HStack(spacing: 3) {
+                    Image(systemName: "doc.text")
+                        .font(.system(size: 9))
+                    Text("\(session.noteCount)")
+                        .font(.system(size: 10, design: .monospaced))
+                }
+                .foregroundStyle(Color.secondary)
+            }
+            .padding(.top, 4)
+        }
+        .padding(12)
+        .frame(width: 150, alignment: .leading)
+        .background(Color.cardBackground.opacity(0.3))
+        .businessBorder(cornerRadius: 8)
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    HomeView()
+        .modelContainer(for: CelestiaSession.self, inMemory: true)
+}

+ 226 - 0
CelestiaTrace/Views/Profile/Auth/AuthModalView.swift

@@ -0,0 +1,226 @@
+import SwiftUI
+
+/// Modal Sheet allowing users to Login or Register an account locally.
+struct AuthModalView: View {
+    @Environment(\.dismiss) private var dismiss
+    @ObservedObject var authManager: AuthManager = .shared
+    
+    @State private var selectedTab: AuthTab = .login
+    @State private var usernameInput: String = ""
+    @State private var identifierInput: String = ""
+    @State private var passwordInput: String = ""
+    @State private var confirmPasswordInput: String = ""
+    @State private var errorMessage: String?
+    
+    enum AuthTab {
+        case login
+        case register
+    }
+    
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+            
+            VStack(spacing: 20) {
+                // Header
+                HStack {
+                    Spacer()
+                    Button {
+                        dismiss()
+                    } label: {
+                        Image(systemName: "xmark.circle.fill")
+                            .font(.system(size: 22))
+                            .foregroundStyle(Color.secondary.opacity(0.6))
+                    }
+                }
+                .padding(.horizontal, 16)
+                .padding(.top, 16)
+                
+                // Title
+                VStack(spacing: 6) {
+                    Text(selectedTab == .login ? "账号登录" : "创建新账号")
+                        .font(.system(size: 22, weight: .bold))
+                        .foregroundStyle(Color.primary)
+                    
+                    Text(selectedTab == .login ? "登录以同步录音并绑定专属录音设备" : "注册后解锁全功能本地沙盒与云同步机制")
+                        .font(.system(size: 12))
+                        .foregroundStyle(Color.secondary)
+                        .multilineTextAlignment(.center)
+                }
+                .padding(.horizontal, 24)
+                
+                // Tab Picker
+                HStack(spacing: 0) {
+                    tabButton(title: "登录", tab: .login)
+                    tabButton(title: "注册账号", tab: .register)
+                }
+                .background(Color.cardBackground.opacity(0.4))
+                .cornerRadius(8)
+                .padding(.horizontal, 24)
+                .padding(.top, 8)
+                
+                // Input Fields
+                VStack(spacing: 14) {
+                    if selectedTab == .register {
+                        inputField(
+                            icon: "person",
+                            placeholder: "设置昵称 (如: 星痕探索者)",
+                            text: $usernameInput
+                        )
+                    }
+                    
+                    inputField(
+                        icon: "envelope",
+                        placeholder: "手机号或电子邮箱",
+                        text: $identifierInput
+                    )
+                    
+                    inputField(
+                        icon: "lock",
+                        placeholder: "密码 (至少 6 位)",
+                        text: $passwordInput,
+                        isSecure: true
+                    )
+                    
+                    if selectedTab == .register {
+                        inputField(
+                            icon: "lock.shield",
+                            placeholder: "再次确认密码",
+                            text: $confirmPasswordInput,
+                            isSecure: true
+                        )
+                    }
+                    
+                    if let error = errorMessage {
+                        Text(error)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.recordingRed)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                            .padding(.horizontal, 4)
+                    }
+                }
+                .padding(.horizontal, 24)
+                .padding(.top, 12)
+                
+                Spacer()
+                
+                // Submit Button
+                Button {
+                    handleAuthSubmit()
+                } label: {
+                    if authManager.isProcessing {
+                        ProgressView()
+                            .tint(Color.spaceBlack)
+                    } else {
+                        Text(selectedTab == .login ? "安全登录" : "注册并自动登录")
+                            .font(.system(size: 15, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 14)
+                .background(Color.primary)
+                .cornerRadius(8)
+                .padding(.horizontal, 24)
+                .padding(.bottom, 32)
+                .disabled(authManager.isProcessing)
+            }
+        }
+    }
+    
+    private func tabButton(title: String, tab: AuthTab) -> some View {
+        Button {
+            withAnimation(.snappy) {
+                selectedTab = tab
+                errorMessage = nil
+            }
+        } label: {
+            Text(title)
+                .font(.system(size: 14, weight: selectedTab == tab ? .semibold : .regular))
+                .foregroundStyle(selectedTab == tab ? Color.primary : Color.secondary)
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 10)
+                .background(
+                    selectedTab == tab ? Color.cardBackground : Color.clear
+                )
+                .cornerRadius(6)
+                .padding(2)
+        }
+    }
+    
+    private func inputField(icon: String, placeholder: String, text: Binding<String>, isSecure: Bool = false) -> some View {
+        HStack(spacing: 12) {
+            Image(systemName: icon)
+                .font(.system(size: 14))
+                .foregroundStyle(Color.secondary)
+                .frame(width: 20)
+            
+            if isSecure {
+                SecureField(placeholder, text: text)
+                    .font(.system(size: 14))
+            } else {
+                TextField(placeholder, text: text)
+                    .font(.system(size: 14))
+                    .autocapitalization(.none)
+                    .disableAutocorrection(true)
+            }
+        }
+        .padding(.horizontal, 14)
+        .padding(.vertical, 12)
+        .background(Color.cardBackground.opacity(0.3))
+        .businessBorder(cornerRadius: 8)
+    }
+    
+    private func handleAuthSubmit() {
+        errorMessage = nil
+        
+        if selectedTab == .login {
+            guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
+                errorMessage = "请输入手机号或邮箱"
+                return
+            }
+            guard passwordInput.count >= 6 else {
+                errorMessage = "密码不能少于 6 位"
+                return
+            }
+            
+            authManager.login(identifier: identifierInput, password: passwordInput) { success, err in
+                if success {
+                    dismiss()
+                } else {
+                    errorMessage = err ?? "登录失败"
+                }
+            }
+        } else {
+            let name = usernameInput.trimmingCharacters(in: .whitespaces)
+            guard !name.isEmpty else {
+                errorMessage = "请输入用户昵称"
+                return
+            }
+            guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
+                errorMessage = "请输入注册手机号或邮箱"
+                return
+            }
+            guard passwordInput.count >= 6 else {
+                errorMessage = "密码不能少于 6 位"
+                return
+            }
+            guard passwordInput == confirmPasswordInput else {
+                errorMessage = "两次输入的密码不一致"
+                return
+            }
+            
+            authManager.register(username: name, identifier: identifierInput, password: passwordInput) { success, err in
+                if success {
+                    dismiss()
+                } else {
+                    errorMessage = err ?? "注册失败"
+                }
+            }
+        }
+    }
+}
+
+#Preview {
+    AuthModalView()
+}

+ 133 - 0
CelestiaTrace/Views/Profile/Auth/ChangePasswordModalView.swift

@@ -0,0 +1,133 @@
+import SwiftUI
+
+/// Modal Sheet allowing logged-in users to change their password.
+struct ChangePasswordModalView: View {
+    @Environment(\.dismiss) private var dismiss
+    @ObservedObject var authManager: AuthManager = .shared
+    
+    @State private var oldPasswordInput: String = ""
+    @State private var newPasswordInput: String = ""
+    @State private var confirmPasswordInput: String = ""
+    @State private var errorMessage: String?
+    @State private var successToast: String?
+    
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+            
+            VStack(spacing: 20) {
+                // Header
+                HStack {
+                    Text("修改密码")
+                        .font(.system(size: 18, weight: .bold))
+                        .foregroundStyle(Color.primary)
+                    Spacer()
+                    Button {
+                        dismiss()
+                    } label: {
+                        Image(systemName: "xmark.circle.fill")
+                            .font(.system(size: 22))
+                            .foregroundStyle(Color.secondary.opacity(0.6))
+                    }
+                }
+                .padding(.horizontal, 20)
+                .padding(.top, 20)
+                
+                VStack(spacing: 14) {
+                    passwordField(label: "当前原密码", text: $oldPasswordInput, placeholder: "请输入当前账号原密码")
+                    passwordField(label: "新密码", text: $newPasswordInput, placeholder: "请输入新密码 (不少于 6 位)")
+                    passwordField(label: "确认新密码", text: $confirmPasswordInput, placeholder: "请再次输入新密码")
+                    
+                    if let error = errorMessage {
+                        Text(error)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.recordingRed)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                    
+                    if let toast = successToast {
+                        Text(toast)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.green)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                }
+                .padding(.horizontal, 20)
+                .padding(.top, 10)
+                
+                Spacer()
+                
+                Button {
+                    handleChangePassword()
+                } label: {
+                    if authManager.isProcessing {
+                        ProgressView()
+                            .tint(Color.spaceBlack)
+                    } else {
+                        Text("确认修改密码")
+                            .font(.system(size: 15, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 14)
+                .background(Color.primary)
+                .cornerRadius(8)
+                .padding(.horizontal, 20)
+                .padding(.bottom, 24)
+                .disabled(authManager.isProcessing)
+            }
+        }
+    }
+    
+    private func passwordField(label: String, text: Binding<String>, placeholder: String) -> some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text(label)
+                .font(.system(size: 12))
+                .foregroundStyle(Color.secondary)
+            
+            HStack(spacing: 12) {
+                Image(systemName: "lock")
+                    .font(.system(size: 14))
+                    .foregroundStyle(Color.secondary)
+                    .frame(width: 20)
+                
+                SecureField(placeholder, text: text)
+                    .font(.system(size: 14))
+            }
+            .padding(.horizontal, 14)
+            .padding(.vertical, 12)
+            .background(Color.cardBackground.opacity(0.3))
+            .businessBorder(cornerRadius: 8)
+        }
+    }
+    
+    private func handleChangePassword() {
+        errorMessage = nil
+        successToast = nil
+        
+        guard !oldPasswordInput.isEmpty else {
+            errorMessage = "请输入原密码"
+            return
+        }
+        guard newPasswordInput.count >= 6 else {
+            errorMessage = "新密码长度不能少于 6 位"
+            return
+        }
+        guard newPasswordInput == confirmPasswordInput else {
+            errorMessage = "两次输入的无缝新密码不一致"
+            return
+        }
+        
+        authManager.changePassword(oldPassword: oldPasswordInput, newPassword: newPasswordInput) { success, err in
+            if success {
+                successToast = "密码修改成功!请记牢新密码"
+                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+                    dismiss()
+                }
+            } else {
+                errorMessage = err ?? "原密码错误或修改失败"
+            }
+        }
+    }
+}

+ 134 - 0
CelestiaTrace/Views/Profile/Auth/EditProfileModalView.swift

@@ -0,0 +1,134 @@
+import SwiftUI
+
+/// Modal Sheet allowing logged-in users to update their profile (nickname, email, phone).
+struct EditProfileModalView: View {
+    @Environment(\.dismiss) private var dismiss
+    @ObservedObject var authManager: AuthManager = .shared
+    
+    @State private var usernameInput: String = ""
+    @State private var emailInput: String = ""
+    @State private var phoneInput: String = ""
+    @State private var errorMessage: String?
+    @State private var successToast: String?
+    
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+            
+            VStack(spacing: 20) {
+                // Header
+                HStack {
+                    Text("编辑个人资料")
+                        .font(.system(size: 18, weight: .bold))
+                        .foregroundStyle(Color.primary)
+                    Spacer()
+                    Button {
+                        dismiss()
+                    } label: {
+                        Image(systemName: "xmark.circle.fill")
+                            .font(.system(size: 22))
+                            .foregroundStyle(Color.secondary.opacity(0.6))
+                    }
+                }
+                .padding(.horizontal, 20)
+                .padding(.top, 20)
+                
+                VStack(spacing: 14) {
+                    inputSection(label: "用户昵称", icon: "person", text: $usernameInput, placeholder: "请输入新的用户昵称")
+                    inputSection(label: "电子邮箱", icon: "envelope", text: $emailInput, placeholder: "绑定/修改电子邮箱")
+                    inputSection(label: "手机号码", icon: "phone", text: $phoneInput, placeholder: "绑定/修改手机号码")
+                    
+                    if let error = errorMessage {
+                        Text(error)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.recordingRed)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                    
+                    if let toast = successToast {
+                        Text(toast)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.green)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                }
+                .padding(.horizontal, 20)
+                .padding(.top, 10)
+                
+                Spacer()
+                
+                Button {
+                    handleSave()
+                } label: {
+                    if authManager.isProcessing {
+                        ProgressView()
+                            .tint(Color.spaceBlack)
+                    } else {
+                        Text("保存修改")
+                            .font(.system(size: 15, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 14)
+                .background(Color.primary)
+                .cornerRadius(8)
+                .padding(.horizontal, 20)
+                .padding(.bottom, 24)
+                .disabled(authManager.isProcessing)
+            }
+        }
+        .onAppear {
+            if let user = authManager.currentUser {
+                usernameInput = user.username
+                emailInput = user.email ?? ""
+                phoneInput = user.phoneNumber ?? ""
+            }
+        }
+    }
+    
+    private func inputSection(label: String, icon: String, text: Binding<String>, placeholder: String) -> some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text(label)
+                .font(.system(size: 12))
+                .foregroundStyle(Color.secondary)
+            
+            HStack(spacing: 12) {
+                Image(systemName: icon)
+                    .font(.system(size: 14))
+                    .foregroundStyle(Color.secondary)
+                    .frame(width: 20)
+                
+                TextField(placeholder, text: text)
+                    .font(.system(size: 14))
+                    .autocapitalization(.none)
+                    .disableAutocorrection(true)
+            }
+            .padding(.horizontal, 14)
+            .padding(.vertical, 12)
+            .background(Color.cardBackground.opacity(0.3))
+            .businessBorder(cornerRadius: 8)
+        }
+    }
+    
+    private func handleSave() {
+        errorMessage = nil
+        successToast = nil
+        
+        authManager.updateProfile(
+            username: usernameInput,
+            email: emailInput,
+            phoneNumber: phoneInput,
+            avatarURL: nil
+        ) { success, err in
+            if success {
+                successToast = "个人资料更新成功!"
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
+                    dismiss()
+                }
+            } else {
+                errorMessage = err ?? "保存修改失败"
+            }
+        }
+    }
+}

+ 206 - 0
CelestiaTrace/Views/Profile/Devices/DeviceListView.swift

@@ -0,0 +1,206 @@
+import SwiftUI
+
+/// Component embedded in ProfileView displaying user's bound BLE recording devices and empty state.
+struct DeviceListView: View {
+    @ObservedObject var bleManager: BLEManager = .shared
+    @ObservedObject var authManager: AuthManager = .shared
+    
+    var onAddDeviceClicked: () -> Void
+    @State private var deviceToUnbind: BoundDevice?
+    
+    private var userBoundDevices: [BoundDevice] {
+        guard let userId = authManager.currentUser?.id else { return [] }
+        return bleManager.devices(forUserId: userId)
+    }
+    
+    var body: some View {
+        VStack(alignment: .leading, spacing: 14) {
+            // Header Row
+            HStack {
+                HStack(spacing: 8) {
+                    Image(systemName: "mic.badge.plus")
+                        .font(.system(size: 14, weight: .light))
+                        .foregroundStyle(Color.secondary)
+                    
+                    Text("我的录音设备")
+                        .font(.system(size: 15, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                }
+                
+                Spacer()
+                
+                Button {
+                    onAddDeviceClicked()
+                } label: {
+                    HStack(spacing: 4) {
+                        Image(systemName: "plus")
+                            .font(.system(size: 11, weight: .bold))
+                        Text("添加设备")
+                            .font(.system(size: 12, weight: .medium))
+                    }
+                    .foregroundStyle(Color.primary)
+                    .padding(.horizontal, 10)
+                    .padding(.vertical, 5)
+                    .background(Color.primary.opacity(0.08))
+                    .cornerRadius(6)
+                }
+            }
+            
+            // Content State
+            if userBoundDevices.isEmpty {
+                emptyDeviceCard
+            } else {
+                VStack(spacing: 10) {
+                    ForEach(userBoundDevices) { device in
+                        boundDeviceCard(device)
+                    }
+                }
+            }
+        }
+        .confirmationDialog(
+            "确定要解绑设备「\(deviceToUnbind?.name ?? "")」吗?",
+            isPresented: Binding(
+                get: { deviceToUnbind != nil },
+                set: { if !$0 { deviceToUnbind = nil } }
+            ),
+            titleVisibility: .visible
+        ) {
+            Button("解绑设备", role: .destructive) {
+                if let dev = deviceToUnbind {
+                    bleManager.unbindDevice(id: dev.id)
+                }
+            }
+            Button("取消", role: .cancel) { }
+        } message: {
+            Text("解绑后该设备将从你的账号名下移除。")
+        }
+    }
+    
+    // MARK: - Empty Device Card
+    
+    private var emptyDeviceCard: some View {
+        VStack(spacing: 14) {
+            ZStack {
+                Circle()
+                    .fill(Color.primary.opacity(0.04))
+                    .frame(width: 56, height: 56)
+                
+                Image(systemName: "mic.slash")
+                    .font(.system(size: 24, weight: .light))
+                    .foregroundStyle(Color.secondary.opacity(0.6))
+            }
+            
+            VStack(spacing: 4) {
+                Text("暂未绑定录音设备")
+                    .font(.system(size: 14, weight: .medium))
+                    .foregroundStyle(Color.primary)
+                
+                Text("通过低功耗蓝牙绑定设备后,可同步硬件录音及状态管理")
+                    .font(.system(size: 11))
+                    .foregroundStyle(Color.secondary)
+                    .multilineTextAlignment(.center)
+            }
+            
+            Button {
+                onAddDeviceClicked()
+            } label: {
+                HStack(spacing: 6) {
+                    Image(systemName: "wave.3.right")
+                        .font(.system(size: 12))
+                    Text("搜索并绑定蓝牙设备")
+                        .font(.system(size: 13, weight: .medium))
+                }
+                .foregroundStyle(Color.spaceBlack)
+                .padding(.horizontal, 16)
+                .padding(.vertical, 9)
+                .background(Color.primary)
+                .cornerRadius(6)
+            }
+            .padding(.top, 4)
+        }
+        .frame(maxWidth: .infinity)
+        .padding(.vertical, 24)
+        .padding(.horizontal, 16)
+        .background(Color.cardBackground.opacity(0.2))
+        .businessBorder(cornerRadius: 10)
+    }
+    
+    // MARK: - Bound Device Card
+    
+    private func boundDeviceCard(_ device: BoundDevice) -> some View {
+        HStack(spacing: 14) {
+            // Icon
+            ZStack {
+                RoundedRectangle(cornerRadius: 8)
+                    .fill(Color.primary.opacity(0.06))
+                    .frame(width: 44, height: 44)
+                
+                Image(systemName: "mic.fill")
+                    .font(.system(size: 18))
+                    .foregroundStyle(Color.primary)
+            }
+            
+            // Device Details
+            VStack(alignment: .leading, spacing: 4) {
+                HStack(spacing: 8) {
+                    Text(device.name)
+                        .font(.system(size: 14, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                    
+                    // Connection Badge
+                    HStack(spacing: 4) {
+                        Circle()
+                            .fill(device.isConnected ? Color.green : Color.gray)
+                            .frame(width: 6, height: 6)
+                        
+                        Text(device.isConnected ? "已连接" : "未连接")
+                            .font(.system(size: 10, weight: .medium))
+                            .foregroundStyle(device.isConnected ? Color.green : Color.secondary)
+                    }
+                }
+                
+                HStack(spacing: 12) {
+                    if let battery = device.batteryLevel {
+                        HStack(spacing: 3) {
+                            Image(systemName: "battery.75")
+                                .font(.system(size: 10))
+                            Text("\(battery)%")
+                                .font(.system(size: 11, design: .monospaced))
+                        }
+                        .foregroundStyle(Color.secondary)
+                    }
+                    
+                    if let fw = device.firmwareVersion {
+                        Text("固件: \(fw)")
+                            .font(.system(size: 11, design: .monospaced))
+                            .foregroundStyle(Color.secondary)
+                    }
+                }
+            }
+            
+            Spacer()
+            
+            // Unbind Button
+            Button {
+                deviceToUnbind = device
+            } label: {
+                Text("解绑")
+                    .font(.system(size: 12, weight: .regular))
+                    .foregroundStyle(Color.recordingRed)
+                    .padding(.horizontal, 10)
+                    .padding(.vertical, 5)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 6)
+                            .stroke(Color.recordingRed.opacity(0.3), lineWidth: 1)
+                    )
+            }
+        }
+        .padding(14)
+        .background(Color.cardBackground.opacity(0.35))
+        .businessBorder(cornerRadius: 10)
+    }
+}
+
+#Preview {
+    DeviceListView(onAddDeviceClicked: {})
+}

+ 203 - 0
CelestiaTrace/Views/Profile/Devices/DeviceScanView.swift

@@ -0,0 +1,203 @@
+import SwiftUI
+
+/// Modal sheet for scanning nearby BLE audio recording devices and binding them to the logged-in user.
+struct DeviceScanView: View {
+    @Environment(\.dismiss) private var dismiss
+    @ObservedObject var bleManager: BLEManager = .shared
+    @ObservedObject var authManager: AuthManager = .shared
+    
+    @State private var boundSuccessMessage: String?
+    
+    var body: some View {
+        ZStack {
+            Color.spaceBlack.ignoresSafeArea()
+            
+            VStack(spacing: 20) {
+                // Top Header
+                HStack {
+                    VStack(alignment: .leading, spacing: 4) {
+                        Text("搜索与绑定录音设备")
+                            .font(.system(size: 18, weight: .bold))
+                            .foregroundStyle(Color.primary)
+                        
+                        Text("请确保设备已开启蓝牙广播模式并靠拢 iPhone")
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.secondary)
+                    }
+                    
+                    Spacer()
+                    
+                    Button {
+                        bleManager.stopScanning()
+                        dismiss()
+                    } label: {
+                        Image(systemName: "xmark.circle.fill")
+                            .font(.system(size: 22))
+                            .foregroundStyle(Color.secondary.opacity(0.6))
+                    }
+                }
+                .padding(.horizontal, 20)
+                .padding(.top, 20)
+                
+                // Scanning Status Indicator
+                HStack(spacing: 12) {
+                    if bleManager.isScanning {
+                        ProgressView()
+                            .tint(Color.primary)
+                    } else {
+                        Image(systemName: "radiowaves.left.and.right")
+                            .font(.system(size: 16))
+                            .foregroundStyle(Color.primary)
+                    }
+                    
+                    Text(bleManager.isScanning ? "正在扫描附近的蓝牙录音设备..." : "扫描已停止")
+                        .font(.system(size: 13, weight: .medium))
+                        .foregroundStyle(Color.primary)
+                    
+                    Spacer()
+                    
+                    Button {
+                        if bleManager.isScanning {
+                            bleManager.stopScanning()
+                        } else {
+                            bleManager.startScanning()
+                        }
+                    } label: {
+                        Text(bleManager.isScanning ? "停止扫描" : "重新扫描")
+                            .font(.system(size: 12, weight: .regular))
+                            .foregroundStyle(Color.primary)
+                            .padding(.horizontal, 10)
+                            .padding(.vertical, 6)
+                            .overlay(
+                                RoundedRectangle(cornerRadius: 6)
+                                    .stroke(Color.primary.opacity(0.3), lineWidth: 1)
+                            )
+                    }
+                }
+                .padding(14)
+                .background(Color.cardBackground.opacity(0.4))
+                .businessBorder(cornerRadius: 10)
+                .padding(.horizontal, 20)
+                
+                // Success Toast Banner
+                if let successMsg = boundSuccessMessage {
+                    HStack(spacing: 8) {
+                        Image(systemName: "checkmark.circle.fill")
+                            .foregroundStyle(Color.primary)
+                        Text(successMsg)
+                            .font(.system(size: 13, weight: .medium))
+                            .foregroundStyle(Color.primary)
+                    }
+                    .padding(.vertical, 10)
+                    .padding(.horizontal, 16)
+                    .background(Color.primary.opacity(0.1))
+                    .cornerRadius(8)
+                    .transition(.move(edge: .top).combined(with: .opacity))
+                }
+                
+                // Discovered Devices List
+                if bleManager.discoveredDevices.isEmpty {
+                    VStack(spacing: 12) {
+                        Spacer()
+                        Image(systemName: "antenna.radiowaves.left.and.right")
+                            .font(.system(size: 36, weight: .ultraLight))
+                            .foregroundStyle(Color.secondary.opacity(0.4))
+                        
+                        Text("未搜索到可用的蓝牙录音设备")
+                            .font(.system(size: 13))
+                            .foregroundStyle(Color.secondary)
+                        Spacer()
+                    }
+                } else {
+                    ScrollView {
+                        VStack(spacing: 12) {
+                            ForEach(bleManager.discoveredDevices) { device in
+                                discoveredDeviceRow(device)
+                            }
+                        }
+                        .padding(.horizontal, 20)
+                        .padding(.bottom, 20)
+                    }
+                }
+            }
+        }
+        .onAppear {
+            bleManager.startScanning()
+        }
+        .onDisappear {
+            bleManager.stopScanning()
+        }
+    }
+    
+    private func discoveredDeviceRow(_ device: DiscoveredBLEDevice) -> some View {
+        let isAlreadyBound = bleManager.boundDevices.contains(where: { $0.id == device.id })
+        
+        return HStack(spacing: 14) {
+            ZStack {
+                Circle()
+                    .fill(Color.primary.opacity(0.06))
+                    .frame(width: 42, height: 42)
+                
+                Image(systemName: "mic.fill")
+                    .font(.system(size: 16))
+                    .foregroundStyle(Color.primary)
+            }
+            
+            VStack(alignment: .leading, spacing: 4) {
+                Text(device.name)
+                    .font(.system(size: 14, weight: .medium))
+                    .foregroundStyle(Color.primary)
+                
+                HStack(spacing: 8) {
+                    Text("信号强度: \(device.rssi) dBm")
+                        .font(.system(size: 11, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                    
+                    Text("•")
+                        .font(.system(size: 10))
+                        .foregroundStyle(Color.secondary)
+                    
+                    Text("设备标识: " + String(device.peripheralUUID.prefix(8)))
+                        .font(.system(size: 11, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+            
+            Spacer()
+            
+            if isAlreadyBound {
+                Text("已绑定")
+                    .font(.system(size: 12, weight: .regular))
+                    .foregroundStyle(Color.secondary)
+                    .padding(.horizontal, 12)
+                    .padding(.vertical, 6)
+            } else {
+                Button {
+                    guard let userId = authManager.currentUser?.id else { return }
+                    let bound = bleManager.bindDevice(device, userId: userId)
+                    withAnimation {
+                        boundSuccessMessage = "已成功绑定设备「\(bound.name)」"
+                    }
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
+                        dismiss()
+                    }
+                } label: {
+                    Text("绑定设备")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(Color.spaceBlack)
+                        .padding(.horizontal, 14)
+                        .padding(.vertical, 7)
+                        .background(Color.primary)
+                        .cornerRadius(6)
+                }
+            }
+        }
+        .padding(14)
+        .background(Color.cardBackground.opacity(0.3))
+        .businessBorder(cornerRadius: 10)
+    }
+}
+
+#Preview {
+    DeviceScanView()
+}

+ 411 - 0
CelestiaTrace/Views/Profile/ProfileView.swift

@@ -0,0 +1,411 @@
+import SwiftUI
+
+/// Main "我" (Mine) Tab View for user authentication, profile management, cloud sync status, and BLE device management.
+struct ProfileView: View {
+    @ObservedObject var authManager: AuthManager = .shared
+    @ObservedObject var bleManager: BLEManager = .shared
+    
+    @State private var showAuthModal = false
+    @State private var showEditProfileModal = false
+    @State private var showChangePasswordModal = false
+    @State private var showScanDeviceModal = false
+    @State private var showSyncAuthPrompt = false
+    @State private var showLogoutConfirmation = false
+    @State private var syncToastMessage: String?
+    
+    var body: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+                
+                ScrollView {
+                    VStack(spacing: 20) {
+                        // 1. Account Profile Header
+                        accountHeaderCard
+                            .padding(.top, 12)
+                        
+                        // 2. User Management Actions (if logged in)
+                        if authManager.isLoggedIn {
+                            userManagementActionsCard
+                        }
+                        
+                        // 3. Audio Cloud Sync Card
+                        audioSyncCard
+                        
+                        // 4. Device Binding & Management
+                        DeviceListView(
+                            bleManager: bleManager,
+                            authManager: authManager,
+                            onAddDeviceClicked: handleAddDevice
+                        )
+                        
+                        // 5. Account Settings & Security
+                        accountInfoSection
+                        
+                        // Footer
+                        footer
+                            .padding(.top, 12)
+                            .padding(.bottom, 40)
+                    }
+                    .padding(.horizontal, 16)
+                }
+            }
+            .navigationTitle("个人中心")
+            .navigationBarTitleDisplayMode(.inline)
+            .sheet(isPresented: $showAuthModal) {
+                AuthModalView(authManager: authManager)
+            }
+            .sheet(isPresented: $showEditProfileModal) {
+                EditProfileModalView(authManager: authManager)
+            }
+            .sheet(isPresented: $showChangePasswordModal) {
+                ChangePasswordModalView(authManager: authManager)
+            }
+            .sheet(isPresented: $showScanDeviceModal) {
+                DeviceScanView(bleManager: bleManager, authManager: authManager)
+            }
+            .sheet(isPresented: $showSyncAuthPrompt) {
+                SyncAuthPromptModal(onLoginClick: {
+                    showAuthModal = true
+                })
+            }
+            .confirmationDialog("确定要退出登录吗?", isPresented: $showLogoutConfirmation, titleVisibility: .visible) {
+                Button("退出登录", role: .destructive) {
+                    authManager.logout()
+                }
+                Button("取消", role: .cancel) {}
+            }
+        }
+    }
+    
+    // MARK: - 1. Account Header
+    
+    private var accountHeaderCard: some View {
+        VStack(spacing: 16) {
+            if let user = authManager.currentUser {
+                // Authenticated Profile State
+                HStack(spacing: 16) {
+                    ZStack {
+                        Circle()
+                            .fill(Color.primary.opacity(0.12))
+                            .frame(width: 60, height: 60)
+                        
+                        Image(systemName: "person.crop.circle.fill")
+                            .font(.system(size: 34))
+                            .foregroundStyle(Color.primary)
+                    }
+                    
+                    VStack(alignment: .leading, spacing: 6) {
+                        HStack {
+                            Text(user.username)
+                               .font(.system(size: 18, weight: .bold))
+                                .foregroundStyle(Color.primary)
+                            
+                            Text("本地授权")
+                                .font(.system(size: 9, weight: .bold, design: .monospaced))
+                                .foregroundStyle(Color.spaceBlack)
+                                .padding(.horizontal, 6)
+                                .padding(.vertical, 2)
+                                .background(Color.primary)
+                                .cornerRadius(4)
+                        }
+                        
+                        Text(user.email ?? user.phoneNumber ?? ("用户 ID: " + String(user.id.prefix(12))))
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.secondary)
+                    }
+                    
+                    Spacer()
+                    
+                    Button {
+                        showLogoutConfirmation = true
+                    } label: {
+                        Text("退出")
+                            .font(.system(size: 12, weight: .regular))
+                            .foregroundStyle(Color.recordingRed)
+                            .padding(.horizontal, 10)
+                            .padding(.vertical, 5)
+                            .overlay(
+                                RoundedRectangle(cornerRadius: 6)
+                                    .stroke(Color.recordingRed.opacity(0.4), lineWidth: 1)
+                            )
+                    }
+                }
+            } else {
+                // Guest / Unauthenticated State
+                VStack(spacing: 14) {
+                    HStack(spacing: 14) {
+                        ZStack {
+                            Circle()
+                                .fill(Color.primary.opacity(0.06))
+                                .frame(width: 52, height: 52)
+                            
+                            Image(systemName: "person.crop.circle.badge.plus")
+                                .font(.system(size: 24))
+                                .foregroundStyle(Color.secondary)
+                        }
+                        
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text("未登录账号")
+                                .font(.system(size: 16, weight: .semibold))
+                                .foregroundStyle(Color.primary)
+                            
+                            Text("登录/注册后支持全功能体验与 BLE 硬件绑定")
+                                .font(.system(size: 12))
+                                .foregroundStyle(Color.secondary)
+                        }
+                        
+                        Spacer()
+                    }
+                    
+                    Button {
+                        showAuthModal = true
+                    } label: {
+                        Text("注册 / 登录账号")
+                            .font(.system(size: 14, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                            .frame(maxWidth: .infinity)
+                            .padding(.vertical, 11)
+                            .background(Color.primary)
+                            .cornerRadius(8)
+                    }
+                }
+            }
+        }
+        .padding(16)
+        .background(Color.cardBackground.opacity(0.25))
+        .businessBorder(cornerRadius: 12)
+    }
+    
+    // MARK: - 2. User Management Actions
+    
+    private var userManagementActionsCard: some View {
+        VStack(alignment: .leading, spacing: 14) {
+            HStack(spacing: 8) {
+                Image(systemName: "slider.horizontal.3")
+                    .font(.system(size: 14, weight: .light))
+                    .foregroundStyle(Color.secondary)
+                
+                Text("用户资料管理")
+                    .font(.system(size: 15, weight: .semibold))
+                    .foregroundStyle(Color.primary)
+            }
+            
+            HStack(spacing: 12) {
+                Button {
+                    showEditProfileModal = true
+                } label: {
+                    HStack(spacing: 6) {
+                        Image(systemName: "square.and.pencil")
+                            .font(.system(size: 12))
+                        Text("编辑基本资料")
+                            .font(.system(size: 13, weight: .medium))
+                    }
+                    .foregroundStyle(Color.primary)
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 10)
+                    .background(Color.cardBackground.opacity(0.4))
+                    .cornerRadius(8)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 8)
+                            .stroke(Color.primary.opacity(0.2), lineWidth: 1)
+                    )
+                }
+                
+                Button {
+                    showChangePasswordModal = true
+                } label: {
+                    HStack(spacing: 6) {
+                        Image(systemName: "key.fill")
+                            .font(.system(size: 12))
+                        Text("修改安全密码")
+                            .font(.system(size: 13, weight: .medium))
+                    }
+                    .foregroundStyle(Color.primary)
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 10)
+                    .background(Color.cardBackground.opacity(0.4))
+                    .cornerRadius(8)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 8)
+                            .stroke(Color.primary.opacity(0.2), lineWidth: 1)
+                    )
+                }
+            }
+        }
+        .padding(16)
+        .background(Color.cardBackground.opacity(0.25))
+        .businessBorder(cornerRadius: 12)
+    }
+    
+    // MARK: - 3. Audio Cloud Sync Card
+    
+    private var audioSyncCard: some View {
+        VStack(alignment: .leading, spacing: 14) {
+            HStack {
+                HStack(spacing: 8) {
+                    Image(systemName: "icloud.and.arrow.up")
+                        .font(.system(size: 14, weight: .light))
+                        .foregroundStyle(Color.secondary)
+                    
+                    Text("录音云端同步")
+                        .font(.system(size: 15, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                }
+                
+                Spacer()
+                
+                if authManager.isLoggedIn {
+                    HStack(spacing: 4) {
+                        Circle()
+                            .fill(Color.green)
+                            .frame(width: 6, height: 6)
+                        Text("同步功能就绪")
+                            .font(.system(size: 11))
+                            .foregroundStyle(Color.secondary)
+                    }
+                } else {
+                    Text("需要登录")
+                        .font(.system(size: 11))
+                        .foregroundStyle(Color.recordingRed)
+                }
+            }
+            
+            VStack(alignment: .leading, spacing: 8) {
+                HStack {
+                    Text("上次同步时间")
+                        .font(.system(size: 12))
+                        .foregroundStyle(Color.secondary)
+                    Spacer()
+                    if let lastDate = authManager.lastSyncDate {
+                        Text(lastDate.formatted(date: .numeric, time: .shortened))
+                            .font(.system(size: 12, design: .monospaced))
+                            .foregroundStyle(Color.primary)
+                    } else {
+                        Text("暂无同步记录")
+                            .font(.system(size: 12, design: .monospaced))
+                            .foregroundStyle(Color.secondary)
+                    }
+                }
+                
+                if let toast = syncToastMessage {
+                    HStack(spacing: 6) {
+                        Image(systemName: "checkmark.circle.fill")
+                            .foregroundStyle(Color.primary)
+                        Text(toast)
+                            .font(.system(size: 12))
+                            .foregroundStyle(Color.primary)
+                    }
+                    .padding(.top, 2)
+                }
+            }
+            
+            Button {
+                triggerAudioSync()
+            } label: {
+                HStack(spacing: 8) {
+                    if authManager.isSyncing {
+                        ProgressView()
+                            .tint(Color.primary)
+                    } else {
+                        Image(systemName: "arrow.triangle.2.circlepath")
+                            .font(.system(size: 13))
+                    }
+                    Text(authManager.isSyncing ? "正在同步音频与记录..." : "立即同步录音文件")
+                        .font(.system(size: 13, weight: .medium))
+                }
+                .foregroundStyle(Color.primary)
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 10)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 8)
+                        .stroke(Color.primary.opacity(0.3), lineWidth: 1)
+                )
+            }
+            .disabled(authManager.isSyncing)
+        }
+        .padding(16)
+        .background(Color.cardBackground.opacity(0.25))
+        .businessBorder(cornerRadius: 12)
+    }
+    
+    // MARK: - 5. Account Settings Section
+    
+    private var accountInfoSection: some View {
+        VStack(alignment: .leading, spacing: 14) {
+            HStack(spacing: 8) {
+                Image(systemName: "shield.checkerboard")
+                    .font(.system(size: 14, weight: .light))
+                    .foregroundStyle(Color.secondary)
+                Text("安全与云端合规")
+                    .font(.system(size: 15, weight: .semibold))
+                    .foregroundStyle(Color.primary)
+            }
+            
+            VStack(spacing: 10) {
+                infoRow(label: "数据存储模式", value: "沙盒 SQLite / UserDefaults")
+                infoRow(label: "后端接口状态", value: "已适配 Local & Remote 两套架构")
+                infoRow(label: "数据加密机制", value: "AES-256 本地私钥保护")
+            }
+        }
+        .padding(16)
+        .background(Color.cardBackground.opacity(0.25))
+        .businessBorder(cornerRadius: 12)
+    }
+    
+    private func infoRow(label: String, value: String) -> some View {
+        HStack {
+            Text(label)
+                .font(.system(size: 12))
+                .foregroundStyle(Color.secondary)
+            Spacer()
+            Text(value)
+                .font(.system(size: 12, weight: .regular, design: .monospaced))
+                .foregroundStyle(Color.primary)
+        }
+    }
+    
+    // MARK: - Actions
+    
+    private func handleAddDevice() {
+        if authManager.isLoggedIn {
+            showScanDeviceModal = true
+        } else {
+            showAuthModal = true
+        }
+    }
+    
+    private func triggerAudioSync() {
+        authManager.syncRecordings {
+            showSyncAuthPrompt = true
+        } onComplete: { success in
+            if success {
+                withAnimation {
+                    syncToastMessage = "录音数据与元数据已成功同步!"
+                }
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
+                    syncToastMessage = nil
+                }
+            }
+        }
+    }
+    
+    // MARK: - Footer
+    
+    private var footer: some View {
+        VStack(spacing: 4) {
+            Text("CELESTIA TRACE USER & DEVICE")
+                .font(.system(size: 10, weight: .bold, design: .monospaced))
+                .foregroundStyle(Color.secondary.opacity(0.4))
+                .tracking(2)
+            
+            Text("安全本地注册与极简日志空间")
+                .font(.system(size: 9))
+                .foregroundStyle(Color.secondary.opacity(0.3))
+        }
+    }
+}
+
+#Preview {
+    ProfileView()
+}

+ 429 - 0
CelestiaTrace/Views/Recording/ActiveRecordingView.swift

@@ -0,0 +1,429 @@
+import SwiftUI
+import SwiftData
+
+// MARK: - ActiveRecordingView
+/// The live recording screen showing real-time waveform, elapsed time,
+/// and quick-action buttons for adding photos and notes during a session.
+/// Fully redesigned to use a minimalist business-focused line-drawn UI.
+struct ActiveRecordingView: View {
+    @Environment(\.modelContext) private var modelContext
+    @Environment(\.dismiss) private var dismiss
+
+    let session: CelestiaSession
+    var onFinishRecording: (() -> Void)? = nil
+
+    @State private var recordingVM = RecordingViewModel()
+    @State private var showNoteSheet = false
+    @State private var showCamera = false
+    @State private var noteText = ""
+    @State private var latestEvent: CelestiaTimelineEvent?
+    @State private var latestEventVisible = false
+
+    var body: some View {
+        ZStack {
+            // Dynamic clean background
+            Color.spaceBlack
+                .ignoresSafeArea()
+
+            VStack(spacing: 0) {
+                Spacer()
+                    .frame(height: 20)
+
+                // Top Header Block (Timecode + Indicator)
+                VStack(spacing: 16) {
+                    timecodeSection
+                    recordingIndicator
+                }
+                .padding(.bottom, 24)
+
+                // Live Waveform
+                waveformSection
+                    .padding(.horizontal, 24)
+                    .padding(.bottom, 20)
+
+                // Latest Event Card
+                latestEventCard
+                    .padding(.horizontal, 24)
+                    .padding(.bottom, 20)
+
+                Spacer()
+
+                // Action Buttons
+                actionButtons
+                    .padding(.horizontal, 48)
+                    .padding(.bottom, 32)
+
+                // End Recording
+                endRecordingButton
+                    .padding(.bottom, 24)
+            }
+        }
+        .toolbar(.hidden, for: .tabBar)
+        .toolbar(.hidden, for: .navigationBar)
+        .navigationBarHidden(true)
+        .navigationBarBackButtonHidden(true)
+        .sheet(isPresented: $showNoteSheet) {
+            noteInputSheet
+        }
+        .fullScreenCover(isPresented: $showCamera) {
+            ImagePicker(sourceType: UIImagePickerController.isSourceTypeAvailable(.camera) ? .camera : .photoLibrary) { image in
+                saveImageAndAddEvent(image)
+            }
+        }
+        .onAppear {
+            recordingVM.startRecording()
+        }
+        .onDisappear {
+            if recordingVM.isRecording {
+                recordingVM.stopRecording()
+            }
+        }
+    }
+
+    // MARK: - Timecode
+
+    private var timecodeSection: some View {
+        Text(recordingVM.elapsedTimeFormatted)
+            .font(.system(size: 52, weight: .light))
+            .monospacedDigit()
+            .foregroundStyle(Color.primary)
+            .contentTransition(.numericText(countsDown: false))
+            .frame(height: 64)
+    }
+
+    // MARK: - Recording Indicator
+
+    private var recordingIndicator: some View {
+        HStack(spacing: 8) {
+            TimelineView(.periodic(from: .now, by: 1.0)) { context in
+                let isEven = Int(context.date.timeIntervalSince1970) % 2 == 0
+                Circle()
+                    .fill(recordingVM.isPaused ? Color.secondary : Color.recordingRed)
+                    .frame(width: 8, height: 8)
+                    .opacity(recordingVM.isPaused ? 0.6 : (isEven ? 1.0 : 0.3))
+                    .animation(.easeInOut(duration: 0.5), value: isEven)
+            }
+            .frame(width: 10, height: 10)
+
+            Text(recordingVM.isPaused ? "已暂停" : "正在录音")
+                .font(.system(size: 11, weight: .semibold, design: .monospaced))
+                .foregroundStyle(recordingVM.isPaused ? Color.secondary : Color.recordingRed)
+                .tracking(2)
+
+            // Session title
+            Text("· \(session.title)")
+                .font(.system(size: 11, weight: .regular))
+                .foregroundStyle(Color.secondary.opacity(0.7))
+                .lineLimit(1)
+        }
+        .padding(.horizontal, 10)
+        .padding(.vertical, 4)
+        .businessBorder(cornerRadius: 6)
+    }
+
+    // MARK: - Waveform
+
+    private var waveformSection: some View {
+        VStack(spacing: 0) {
+            LiveWaveformView(samples: recordingVM.waveformSamples)
+                .frame(height: 90)
+
+            // Graphical VU Meter + Numeric Display
+            HStack(spacing: 10) {
+                Text("音量电平")
+                    .font(.system(size: 9, weight: .semibold, design: .monospaced))
+                    .foregroundStyle(Color.secondary.opacity(0.6))
+                    .tracking(1.5)
+
+                AmplitudeLevelMeterView(amplitude: recordingVM.currentAmplitude)
+
+                Spacer(minLength: 0)
+
+                Text(String(format: "%02.0f%%", recordingVM.currentAmplitude * 100))
+                    .font(.system(size: 9, weight: .medium, design: .monospaced))
+                    .foregroundStyle(recordingVM.currentAmplitude > 0.85 ? Color.recordingRed : Color.secondary)
+            }
+            .padding(.horizontal, 4)
+            .padding(.top, 10)
+        }
+        .frame(height: 114)
+    }
+
+    // MARK: - Latest Event Card
+
+    private var latestEventCard: some View {
+        ZStack { // Fix: Use ZStack instead of Group to enforce the fixed frame even when empty
+            if let event = latestEvent, latestEventVisible {
+                HStack(spacing: 12) {
+                    Image(systemName: event.eventIcon)
+                        .font(.system(size: 12))
+                        .foregroundStyle(Color.primary)
+                        .frame(width: 28, height: 28)
+                        .businessBorder(cornerRadius: 14)
+
+                    VStack(alignment: .leading, spacing: 2) {
+                        Text(eventLabel(for: event.eventType))
+                            .font(.system(size: 11, weight: .semibold))
+                            .foregroundStyle(Color.primary)
+
+                        if let text = event.textContent {
+                            Text(text)
+                               .font(.system(size: 12))
+                               .foregroundStyle(Color.secondary)
+                               .lineLimit(1)
+                        }
+                    }
+
+                    Spacer()
+
+                    Text(event.relativeTimeFormatted)
+                        .font(.system(size: 11, weight: .regular, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                }
+                .padding(12)
+                .background(Color.cardBackground.opacity(0.4))
+                .businessBorder(cornerRadius: 8)
+                .transition(.asymmetric(
+                    insertion: .move(edge: .bottom).combined(with: .opacity),
+                    removal: .opacity
+                ))
+            }
+        }
+        .frame(height: 56)
+        .animation(.spring(response: 0.35, dampingFraction: 0.8), value: latestEvent?.id)
+    }
+
+    // MARK: - Action Buttons
+
+    private var actionButtons: some View {
+        HStack(spacing: 40) {
+            // Camera button
+            actionButton(
+                icon: "camera",
+                label: "拍照打点"
+            ) {
+                capturePhoto()
+            }
+
+            // Note button
+            actionButton(
+                icon: "doc.text",
+                label: "文字笔记"
+            ) {
+                showNoteSheet = true
+            }
+        }
+    }
+
+    private func actionButton(
+        icon: String,
+        label: String,
+        action: @escaping () -> Void
+    ) -> some View {
+        Button(action: action) {
+            VStack(spacing: 10) {
+                ZStack {
+                    Circle()
+                        .fill(Color.primary.opacity(0.02))
+                        .frame(width: 60, height: 60)
+                        .businessBorder(cornerRadius: 30)
+
+                    Image(systemName: icon)
+                        .font(.system(size: 20, weight: .light))
+                        .foregroundStyle(Color.primary)
+                }
+
+                Text(label)
+                    .font(.system(size: 11, weight: .regular))
+                    .foregroundStyle(Color.secondary)
+            }
+        }
+        .buttonStyle(.plain)
+    }
+
+    // MARK: - End Recording Button
+
+    private var endRecordingButton: some View {
+        Button {
+            endRecording()
+        } label: {
+            VStack(spacing: 6) {
+                Text("双击结束录制")
+                    .font(.system(size: 13, weight: .medium))
+                    .foregroundStyle(Color.recordingRed)
+                    .padding(.horizontal, 24)
+                    .padding(.vertical, 10)
+                    .businessBorder(cornerRadius: 8)
+                
+                Text("双击即可完成本次现场录音")
+                    .font(.system(size: 8, weight: .medium, design: .monospaced))
+                    .foregroundStyle(Color.secondary.opacity(0.5))
+                    .tracking(1.5)
+            }
+        }
+        .highPriorityGesture(
+            TapGesture(count: 2).onEnded {
+                endRecording()
+            }
+        )
+        .buttonStyle(.plain)
+    }
+
+    // MARK: - Note Input Sheet
+
+    private var noteInputSheet: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+
+                VStack(spacing: 20) {
+                    TextField("输入笔记内容...", text: $noteText, axis: .vertical)
+                        .textFieldStyle(.plain)
+                        .font(.system(size: 15))
+                        .foregroundStyle(Color.primary)
+                        .padding(14)
+                        .frame(minHeight: 120, alignment: .top)
+                        .background(Color.cardBackground.opacity(0.5))
+                        .businessBorder(cornerRadius: 8)
+
+                    Button {
+                        addNote()
+                    } label: {
+                        Text("保存笔记")
+                            .font(.system(size: 14, weight: .semibold))
+                            .foregroundStyle(Color.spaceBlack)
+                            .frame(maxWidth: .infinity)
+                            .padding(.vertical, 12)
+                            .background(Color.primary)
+                            .cornerRadius(8)
+                    }
+                    .disabled(noteText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
+
+                    Spacer()
+                }
+                .padding(24)
+            }
+            .navigationTitle("添加笔记")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("取消") {
+                        showNoteSheet = false
+                        noteText = ""
+                    }
+                    .foregroundStyle(Color.secondary)
+                }
+            }
+        }
+        .presentationDetents([.medium])
+        .presentationDragIndicator(.visible)
+        .presentationBackground(Color.spaceBlack)
+    }
+
+    // MARK: - Actions
+
+    private func capturePhoto() {
+        showCamera = true
+    }
+
+    private func saveImageAndAddEvent(_ image: UIImage) {
+        guard let data = image.jpegData(compressionQuality: 0.8) else { return }
+        let filename = "photo_\(UUID().uuidString).jpg"
+        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
+        let fileURL = documentsURL.appendingPathComponent(filename)
+        do {
+            try data.write(to: fileURL)
+            let event = recordingVM.addPhotoEvent(to: session, localFilePath: filename)
+            HapticManager.trigger(.photoCapture)
+            showLatestEvent(event)
+        } catch {
+            print("[ActiveRecordingView] Failed to save captured photo: \(error.localizedDescription)")
+        }
+    }
+
+    private func addNote() {
+        let text = noteText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !text.isEmpty else { return }
+
+        let event = recordingVM.addNoteEvent(to: session, text: text)
+
+        HapticManager.trigger(.noteAdded)
+        showLatestEvent(event)
+
+        noteText = ""
+        showNoteSheet = false
+    }
+
+    private func endRecording() {
+        recordingVM.stopRecording()
+        if let fileURL = recordingVM.outputFileURL {
+            session.localAudioPath = fileURL.path
+        }
+        session.endTime = Date()
+        try? modelContext.save()
+        HapticManager.trigger(.recordStop)
+        dismiss()
+        onFinishRecording?()
+    }
+
+    private func showLatestEvent(_ event: CelestiaTimelineEvent) {
+        withAnimation {
+            latestEvent = event
+            latestEventVisible = true
+        }
+        // Auto-dismiss after a few seconds
+        DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
+            withAnimation {
+                latestEventVisible = false
+            }
+        }
+    }
+
+    // MARK: - Helpers
+
+    private func eventLabel(for type: String) -> String {
+        switch type {
+        case "PHOTO": return "已捕获照片"
+        case "NOTE": return "已保存笔记"
+        case "MARKER": return "标记点"
+        case "VOICE": return "音轨事件"
+        default: return "事件"
+        }
+    }
+}
+
+// MARK: - Amplitude Level Meter View
+
+/// A minimalist graphical VU meter bar visualizing real-time audio amplitude.
+private struct AmplitudeLevelMeterView: View {
+    let amplitude: Float // 0.0 to 1.0
+
+    private let totalSegments: Int = 16
+
+    var body: some View {
+        HStack(spacing: 3) {
+            ForEach(0..<totalSegments, id: \.self) { index in
+                let threshold = Float(index) / Float(totalSegments)
+                let isFilled = amplitude > threshold
+                let isPeak = index >= totalSegments - 2
+
+                RoundedRectangle(cornerRadius: 1)
+                    .fill(
+                        isFilled
+                            ? (isPeak ? Color.recordingRed : Color.primary.opacity(0.85))
+                            : Color.primary.opacity(0.12)
+                    )
+                    .frame(height: isFilled ? (isPeak ? 7 : 5) : 3)
+                    .animation(.spring(response: 0.15, dampingFraction: 0.75), value: amplitude)
+            }
+        }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    let session = CelestiaSession(title: "Preview Session")
+    ActiveRecordingView(session: session)
+        .modelContainer(for: CelestiaSession.self, inMemory: true)
+}

+ 119 - 0
CelestiaTrace/Views/Recording/LiveWaveformView.swift

@@ -0,0 +1,119 @@
+import SwiftUI
+
+// MARK: - LiveWaveformView
+
+/// A real-time audio waveform visualization rendered via SwiftUI `Canvas`.
+/// Uses a highly minimalist, monochrome, line-drawn style suitable for business applications.
+struct LiveWaveformView: View {
+
+    /// Audio amplitude samples (0.0–1.0). Most recent sample is last.
+    let samples: [Float]
+
+    /// Height of the waveform view.
+    var height: CGFloat = 80
+
+    /// Width of each bar in points.
+    private let barWidth: CGFloat = 1.0
+
+    /// Spacing between bars in points.
+    private let barSpacing: CGFloat = 2.0
+
+    /// Minimum bar height to ensure visibility even at zero amplitude.
+    private let minBarHeight: CGFloat = 1.0
+
+    var body: some View {
+        Canvas { context, size in
+            let totalBarSlot = barWidth + barSpacing
+            let maxBars = Int(size.width / totalBarSlot)
+            let maxHeight = size.height
+            let centerY = size.height / 2
+
+            // Take the most recent N samples that fit on screen
+            let visibleSamples: [Float]
+            if samples.count > maxBars {
+                visibleSamples = Array(samples.suffix(maxBars))
+            } else {
+                visibleSamples = samples
+            }
+
+            let barCount = visibleSamples.count
+
+            // Draw center reference line
+            let centerLinePath = Path { path in
+                path.move(to: CGPoint(x: 0, y: centerY))
+                path.addLine(to: CGPoint(x: size.width, y: centerY))
+            }
+            context.stroke(
+                centerLinePath,
+                with: .color(Color.primary.opacity(0.1)),
+                lineWidth: 1
+            )
+
+            // Draw bars right-aligned (most recent on the right)
+            for index in 0..<barCount {
+                let sample = CGFloat(max(visibleSamples[index], 0))
+                let barHeight = max(sample * maxHeight * 0.75, minBarHeight)
+
+                // Position: right-aligned
+                let xOffset = size.width - CGFloat(barCount - index) * totalBarSlot
+
+                let barRect = CGRect(
+                    x: xOffset,
+                    y: centerY - barHeight / 2,
+                    width: barWidth,
+                    height: barHeight
+                )
+
+                let barPath = Path(barRect)
+
+                // Opacity fade on the left edge (older samples fade out)
+                let fadeWidth = 0.25 // 25% of visible area fades
+                let opacity: Double
+                if barCount > 1 {
+                    let normalizedX = CGFloat(index) / CGFloat(barCount - 1)
+                    if normalizedX < fadeWidth {
+                        opacity = Double(normalizedX / fadeWidth)
+                    } else {
+                        opacity = 1.0
+                    }
+                } else {
+                    opacity = 1.0
+                }
+
+                context.opacity = opacity
+                // Simple monochrome fill (Color.primary)
+                context.fill(barPath, with: .color(Color.primary.opacity(0.85)))
+                
+                context.opacity = 1.0 // Reset
+            }
+        }
+        .frame(height: height)
+        .background(
+            RoundedRectangle(cornerRadius: 8)
+                .fill(Color.primary.opacity(0.01)) // Semi-transparent minimal background
+        )
+        .businessBorder(cornerRadius: 8) // Thin outlined border
+    }
+}
+
+// MARK: - Preview
+
+#Preview("Waveform — Active") {
+    ZStack {
+        Color.spaceBlack.ignoresSafeArea()
+
+        VStack(spacing: 20) {
+            LiveWaveformView(
+                samples: (0..<80).map { _ in Float.random(in: 0.05...0.9) }
+            )
+            .padding(.horizontal)
+
+            LiveWaveformView(
+                samples: (0..<40).map { i in
+                    Float(sin(Double(i) * 0.3) * 0.4 + 0.5)
+                }
+            )
+            .padding(.horizontal)
+        }
+    }
+}

+ 288 - 0
CelestiaTrace/Views/Settings/SettingsView.swift

@@ -0,0 +1,288 @@
+import SwiftUI
+
+// MARK: - SettingsView
+/// The settings screen with storage info, debug toggles,
+/// background keep-alive guide, and app branding.
+/// Redesigned to use a minimalist wireframe business style.
+struct SettingsView: View {
+    @State private var showClearCacheAlert = false
+
+    // Mock storage values
+    private let usedStorage: Double = 1.2
+    private let totalStorage: Double = 5.0
+
+    var body: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+
+                ScrollView {
+                    VStack(spacing: 20) {
+                        // Section: Storage
+                        storageSection
+                            .padding(.top, 12)
+
+                        // Section: Debug
+                        debugSection
+
+                        // Section: Keep-Alive Guide
+                        keepAliveSection
+
+                        // Section: About
+                        aboutSection
+
+                        // Footer
+                        footer
+                            .padding(.top, 12)
+                            .padding(.bottom, 40)
+                    }
+                    .padding(.horizontal, 16)
+                }
+            }
+            .navigationTitle("设置")
+            .navigationBarTitleDisplayMode(.inline)
+            .alert("清理缓存", isPresented: $showClearCacheAlert) {
+                Button("取消", role: .cancel) { }
+                Button("清理", role: .destructive) {
+                    HapticManager.trigger(.tapFeedback)
+                }
+            } message: {
+                Text("将清除所有本地缓存数据。此操作不可撤销。")
+            }
+        }
+    }
+
+    // MARK: - Storage Section
+
+    private var storageSection: some View {
+        settingsCard {
+            VStack(alignment: .leading, spacing: 16) {
+                sectionHeader(icon: "internaldrive", title: "存储空间")
+
+                // Thin storage bar
+                VStack(alignment: .leading, spacing: 8) {
+                    GeometryReader { geometry in
+                        ZStack(alignment: .leading) {
+                            // Background bar
+                            RoundedRectangle(cornerRadius: 3)
+                                .fill(Color.primary.opacity(0.06))
+                                .frame(height: 5)
+
+                            // Used portion
+                            RoundedRectangle(cornerRadius: 3)
+                                .fill(Color.primary)
+                                .frame(
+                                    width: geometry.size.width * (usedStorage / totalStorage),
+                                    height: 5
+                                )
+                        }
+                    }
+                    .frame(height: 5)
+
+                    HStack {
+                        Text(String(format: "已使用 %.1f GB", usedStorage))
+                            .font(.system(size: 11, weight: .regular))
+                            .foregroundStyle(Color.primary.opacity(0.8))
+
+                        Spacer()
+
+                        Text(String(format: "总容量 %.0f GB", totalStorage))
+                            .font(.system(size: 11, weight: .regular))
+                            .foregroundStyle(Color.secondary)
+                    }
+                }
+
+                // Clear cache button (minimal red outline)
+                Button {
+                    showClearCacheAlert = true
+                } label: {
+                    HStack(spacing: 6) {
+                        Image(systemName: "trash")
+                            .font(.system(size: 12, weight: .light))
+                        Text("清理缓存")
+                            .font(.system(size: 13, weight: .regular))
+                    }
+                    .foregroundStyle(Color.recordingRed)
+                    .padding(.vertical, 8)
+                    .frame(maxWidth: .infinity)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 6)
+                            .stroke(Color.recordingRed.opacity(0.3), lineWidth: 1)
+                    )
+                }
+                .padding(.top, 4)
+            }
+        }
+    }
+
+    // MARK: - Debug Section
+
+    private var debugSection: some View {
+        settingsCard {
+            VStack(alignment: .leading, spacing: 14) {
+                sectionHeader(icon: "hammer", title: "开发调试")
+
+                // Data Storage Mode
+                VStack(alignment: .leading, spacing: 4) {
+                    HStack {
+                        Text("数据存储模式")
+                            .font(.system(size: 13, weight: .regular))
+                            .foregroundStyle(Color.primary)
+                        Spacer()
+                        Text("纯本地保存")
+                            .font(.system(size: 13, weight: .medium))
+                            .foregroundStyle(Color.secondary)
+                    }
+
+                    Text("所有会议记录均保存在设备沙盒 SQLite 中,无网络上传")
+                        .font(.system(size: 10))
+                        .foregroundStyle(Color.secondary.opacity(0.6))
+                }
+            }
+        }
+    }
+
+    // MARK: - Keep-Alive Section
+
+    private var keepAliveSection: some View {
+        settingsCard {
+            VStack(alignment: .leading, spacing: 14) {
+                sectionHeader(icon: "shield", title: "后台运行保障")
+
+                Text("为保障全局不中断录音正常运行,请手动开启以下系统权限:")
+                    .font(.system(size: 12))
+                    .foregroundStyle(Color.secondary)
+
+                VStack(alignment: .leading, spacing: 12) {
+                    keepAliveStep(
+                        number: 1,
+                        title: "开启后台音频",
+                        description: "Xcode 开启后台模式 -> 音频与 AirPlay",
+                        isChecked: true
+                    )
+
+                    keepAliveStep(
+                        number: 2,
+                        title: "禁用自动锁屏",
+                        description: "系统设置 -> 显示与亮度 -> 自动锁定 -> 永不",
+                        isChecked: true
+                    )
+
+                    keepAliveStep(
+                        number: 3,
+                        title: "允许始终访问定位",
+                        description: "系统设置 -> 隐私与安全性 -> 定位服务 -> 现场记录 -> 始终允许",
+                        isChecked: false
+                    )
+                }
+                .padding(.top, 4)
+            }
+        }
+    }
+
+    private func keepAliveStep(number: Int, title: String, description: String, isChecked: Bool) -> some View {
+        HStack(alignment: .top, spacing: 10) {
+            ZStack {
+                Circle()
+                    .fill(Color.primary.opacity(isChecked ? 0.05 : 0.01))
+                    .frame(width: 24, height: 24)
+                    .businessBorder(cornerRadius: 12)
+
+                if isChecked {
+                    Image(systemName: "checkmark")
+                        .font(.system(size: 10, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                } else {
+                    Text("\(number)")
+                        .font(.system(size: 10, weight: .medium, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+
+            VStack(alignment: .leading, spacing: 2) {
+                Text(title)
+                    .font(.system(size: 13, weight: .medium))
+                    .foregroundStyle(Color.primary)
+
+                Text(description)
+                    .font(.system(size: 11))
+                    .foregroundStyle(Color.secondary.opacity(0.8))
+            }
+        }
+    }
+
+    // MARK: - About Section
+
+    private var aboutSection: some View {
+        settingsCard {
+            VStack(spacing: 14) {
+                sectionHeader(icon: "info.circle", title: "关于")
+
+                VStack(spacing: 10) {
+                    infoRow(label: "应用名称", value: "现场记录 (Celestia Trace)")
+                    infoRow(label: "版本信息", value: "0.1.0 (本地持久化单机版)")
+                    infoRow(label: "底层框架", value: "SwiftUI + SwiftData")
+                    infoRow(label: "系统支持", value: "iOS 17.0+ (模拟器)")
+                }
+                .padding(.top, 4)
+            }
+        }
+    }
+
+    private func infoRow(label: String, value: String) -> some View {
+        HStack {
+            Text(label)
+                .font(.system(size: 12))
+                .foregroundStyle(Color.secondary)
+
+            Spacer()
+
+            Text(value)
+                .font(.system(size: 12, weight: .regular, design: .monospaced))
+                .foregroundStyle(Color.primary)
+        }
+    }
+
+    // MARK: - Footer
+
+    private var footer: some View {
+        VStack(spacing: 4) {
+            Text("星痕现场记录")
+                .font(.system(size: 10, weight: .bold, design: .monospaced))
+                .foregroundStyle(Color.secondary.opacity(0.4))
+                .tracking(2)
+
+            Text("极简专业现场记录工作台")
+                .font(.system(size: 9))
+                .foregroundStyle(Color.secondary.opacity(0.3))
+        }
+    }
+
+    // MARK: - Reusable Components
+
+    private func settingsCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
+        VStack(alignment: .leading, spacing: 0) {
+            content()
+        }
+        .padding(16)
+        .background(Color.cardBackground.opacity(0.15))
+        .businessBorder(cornerRadius: 10)
+    }
+
+    private func sectionHeader(icon: String, title: String) -> some View {
+        HStack(spacing: 8) {
+            Image(systemName: icon)
+                .font(.system(size: 12, weight: .light))
+                .foregroundStyle(Color.secondary)
+            Text(title)
+                .font(.system(size: 14, weight: .semibold))
+                .foregroundStyle(Color.primary)
+        }
+    }
+}
+
+// MARK: - Preview
+
+#Preview {
+    SettingsView()
+}

+ 26 - 0
Package.swift

@@ -0,0 +1,26 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+    name: "CelestiaTrace",
+    platforms: [
+        .iOS(.v17)
+    ],
+    products: [
+        .library(
+            name: "CelestiaTrace",
+            targets: ["CelestiaTrace"]),
+    ],
+    targets: [
+        .target(
+            name: "CelestiaTrace",
+            path: "CelestiaTrace",
+            exclude: [
+                "App/CelestiaTraceApp.swift"
+            ]),
+        .testTarget(
+            name: "CelestiaTraceTests",
+            dependencies: ["CelestiaTrace"],
+            path: "Tests/CelestiaTraceTests")
+    ]
+)

+ 80 - 0
generate_project.rb

@@ -0,0 +1,80 @@
+require 'xcodeproj'
+require 'fileutils'
+
+project_path = 'CelestiaTrace.xcodeproj'
+
+# Remove existing project if any
+FileUtils.rm_rf(project_path)
+
+# Create a new Xcode project
+project = Xcodeproj::Project.new(project_path)
+
+# Create groups
+main_group = project.main_group
+app_group = main_group.new_group('CelestiaTrace', 'CelestiaTrace')
+tests_group = main_group.new_group('CelestiaTraceTests', 'Tests/CelestiaTraceTests')
+
+# Helper to add files recursively
+def add_files_recursively(dir, group, target, project)
+  return unless File.directory?(dir)
+  Dir.foreach(dir) do |entry|
+    next if entry == '.' || entry == '..' || entry == '.DS_Store'
+    path = File.join(dir, entry)
+    
+    if File.directory?(path)
+      # Create subgroup with path relative to the parent group
+      subgroup = group.new_group(entry, entry)
+      add_files_recursively(path, subgroup, target, project)
+    elsif entry.end_with?('.swift')
+      # Add file reference
+      file_ref = group.new_file(entry)
+      # Add to build phases
+      target.add_file_references([file_ref])
+    end
+  end
+end
+
+# Create iOS App target
+app_target = project.new_target(:application, 'CelestiaTrace', :ios, '17.0')
+app_target.build_configurations.each do |config|
+  config.build_settings['SWIFT_VERSION'] = '5.9'
+  config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
+  config.build_settings['TARGETED_DEVICE_FAMILY'] = '1' # 1 for iPhone, 2 for iPad, 1,2 for both
+  config.build_settings['SDKROOT'] = 'iphoneos'
+  config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.celestia.trace'
+  config.build_settings['GENERATE_INFOPLIST_FILE'] = 'YES'
+  config.build_settings['INFOPLIST_KEY_UILaunchScreen_Generation'] = 'YES'
+  config.build_settings['INFOPLIST_KEY_UISupportedInterfaceOrientations'] = 'UIInterfaceOrientationPortrait'
+  config.build_settings['INFOPLIST_KEY_NSMicrophoneUsageDescription'] = '需要麦克风权限以进行会议现场的音轨录音'
+  config.build_settings['INFOPLIST_KEY_NSCameraUsageDescription'] = '需要相机权限以在现场录制中拍摄照片记录'
+  config.build_settings['INFOPLIST_KEY_NSPhotoLibraryUsageDescription'] = '需要相册权限以选择照片记录'
+  config.build_settings['INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription'] = '需要蓝牙权限以扫描和绑定 BLE 录音设备'
+  config.build_settings['INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription'] = '需要蓝牙权限以与录音设备传输数据'
+  config.build_settings['ASSETCATALOG_COMPILER_APPICON_NAME'] = 'AppIcon'
+  config.build_settings['CODE_SIGN_STYLE'] = 'Automatic'
+  # Enable SwiftData & SwiftUI previews support
+  config.build_settings['DEVELOPMENT_ASSET_PATHS'] = '""'
+end
+
+# Create iOS Test target
+test_target = project.new_target(:unit_test_bundle, 'CelestiaTraceTests', :ios, '17.0')
+test_target.build_configurations.each do |config|
+  config.build_settings['SWIFT_VERSION'] = '5.9'
+  config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
+  config.build_settings['SDKROOT'] = 'iphoneos'
+  config.build_settings['GENERATE_INFOPLIST_FILE'] = 'YES'
+  config.build_settings['TEST_TARGET_NAME'] = 'CelestiaTrace'
+  config.build_settings['BUNDLE_LOADER'] = '$(TEST_HOST)'
+  config.build_settings['TEST_HOST'] = '$(BUILT_PRODUCTS_DIR)/CelestiaTrace.app/CelestiaTrace'
+end
+
+# Set up target dependency: Test target depends on App target
+test_target.add_dependency(app_target)
+
+# Add files to targets
+add_files_recursively('CelestiaTrace', app_group, app_target, project)
+add_files_recursively('Tests/CelestiaTraceTests', tests_group, test_target, project)
+
+# Save project
+project.save
+puts "Successfully created CelestiaTrace.xcodeproj"