feat: Implement Bottom Navigation and Basic UI for iOS and Desktop

- Introduce a bottom navigation bar with "Today," "Weekly," and "Settings" tabs for the iOS app.
- Implement a sidebar navigation for the desktop app with Home, Analytics, Reports, and Settings.
- Add Weekly stats with a bar chart and summary for the iOS app.
- Add Settings view with basic settings options for iOS app.
- Add a Progress Ring for displaying "Today" stats in iOS app.
- Add app usage data list to "Today" tab in iOS app.
- Implement dark theme toggle for desktop app.
- Implement collapsible sidebar in desktop app.
- Refine UI of navigation items for desktop app.
- Add basic placeholders for the desktop app UI.
This commit is contained in:
grtsinry43 2025-04-23 01:00:21 +08:00
parent 1c61774c04
commit cfa6f13cdd
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
5 changed files with 439 additions and 110 deletions

View File

@ -4,6 +4,8 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@ -26,6 +28,7 @@ import androidx.compose.material3.TextButton
fun DesktopApp() {
var selectedItem by remember { mutableStateOf(0) }
var isDarkTheme by remember { mutableStateOf(false) }
var isSidebarCollapsed by remember { mutableStateOf(false) } // 新增状态变量
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFF5F5F5)
val surfaceColor = if (isDarkTheme) Color(0xFF2D2D2D) else Color.White
@ -43,7 +46,7 @@ fun DesktopApp() {
// Sidebar
Box(
modifier = Modifier
.width(200.dp)
.width(if (isSidebarCollapsed) 60.dp else 200.dp) // 动态宽度
.fillMaxHeight()
.background(surfaceColor)
.padding(16.dp)
@ -52,6 +55,7 @@ fun DesktopApp() {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (!isSidebarCollapsed) {
Text(
"Activity Analyzer",
fontSize = 16.sp,
@ -59,45 +63,50 @@ fun DesktopApp() {
color = textColor
)
Spacer(modifier = Modifier.height(32.dp))
}
// Navigation items
NavItem(
icon = Icons.Default.Home,
text = "Home",
text = if (isSidebarCollapsed) "" else "Home", // 根据收缩状态显示文本
isSelected = selectedItem == 0,
onClick = { selectedItem = 0 },
textColor = textColor,
accentColor = accentColor
accentColor = accentColor,
isSidebarCollapsed = isSidebarCollapsed
)
NavItem(
icon = Icons.Default.BarChart,
text = "Analytics",
text = if (isSidebarCollapsed) "" else "Analytics",
isSelected = selectedItem == 1,
onClick = { selectedItem = 1 },
textColor = textColor,
accentColor = accentColor
accentColor = accentColor,
isSidebarCollapsed = isSidebarCollapsed
)
NavItem(
icon = Icons.Default.Dataset,
text = "Reports",
text = if (isSidebarCollapsed) "" else "Reports",
isSelected = selectedItem == 2,
onClick = { selectedItem = 2 },
textColor = textColor,
accentColor = accentColor
accentColor = accentColor,
isSidebarCollapsed = isSidebarCollapsed
)
NavItem(
icon = Icons.Default.Settings,
text = "Settings",
text = if (isSidebarCollapsed) "" else "Settings",
isSelected = selectedItem == 3,
onClick = { selectedItem = 3 },
textColor = textColor,
accentColor = accentColor
accentColor = accentColor,
isSidebarCollapsed = isSidebarCollapsed
)
// Spacer for alignment
Spacer(modifier = Modifier.weight(1f))
// Theme switch
if (!isSidebarCollapsed) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -121,6 +130,19 @@ fun DesktopApp() {
)
}
}
// Collapse/Expand button
IconButton(
onClick = { isSidebarCollapsed = !isSidebarCollapsed },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Icon(
imageVector = if (isSidebarCollapsed) Icons.Default.ChevronRight else Icons.Default.ChevronLeft,
contentDescription = "Toggle Sidebar",
tint = textColor
)
}
}
}
// Main content
@ -147,18 +169,20 @@ fun NavItem(
isSelected: Boolean,
onClick: () -> Unit,
textColor: Color,
accentColor: Color
accentColor: Color,
isSidebarCollapsed: Boolean
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(if (isSelected) accentColor.copy(alpha = 0.1f) else Color.Transparent)
.padding(12.dp)
.clip(RoundedCornerShape(12.dp)) // 更大的圆角
.background(if (isSelected) accentColor.copy(alpha = 0.2f) else Color.Transparent)
.padding(vertical = 12.dp, horizontal = if (isSidebarCollapsed) 8.dp else 16.dp)
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }),
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.CenterStart
) {
Row(
@ -168,13 +192,19 @@ fun NavItem(
Icon(
imageVector = icon,
contentDescription = null,
tint = if (isSelected) accentColor else textColor
tint = if (isSelected) accentColor else textColor,
modifier = Modifier.size(if (isSidebarCollapsed) 32.dp else 24.dp) // 动态调整图标大小
)
if (!isSidebarCollapsed) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium.copy(
color = if (isSelected) accentColor else textColor,
fontSize = 16.sp
fontSize = 16.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
)
}
}
}
}
@ -187,11 +217,13 @@ fun HomeScreen(textColor: Color, surfaceColor: Color) {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
SelectionContainer {
Text(
text = "Welcome Back!",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
}
// Quick actions
Card(
@ -207,11 +239,13 @@ fun HomeScreen(textColor: Color, surfaceColor: Color) {
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
SelectionContainer {
Text(
text = "Quick Actions",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
@ -228,38 +262,38 @@ fun HomeScreen(textColor: Color, surfaceColor: Color) {
}
}
}
}
// Recent activities
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Recent Activities",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
ActivityItem(
title = "Activity Report Generated",
time = "2024-04-12 14:30",
icon = Icons.Default.Description
)
ActivityItem(
title = "Data Imported",
time = "2024-04-12 10:15",
icon = Icons.Default.Upload
)
}
}
}
// // Recent activities
// Card(
// modifier = Modifier
// .fillMaxWidth()
// .clip(RoundedCornerShape(16.dp)),
// colors = CardDefaults.cardColors(
// containerColor = MaterialTheme.colorScheme.surface
// )
// ) {
// Column(
// modifier = Modifier.padding(16.dp),
// verticalArrangement = Arrangement.spacedBy(16.dp)
// ) {
// Text(
// text = "Recent Activities",
// style = MaterialTheme.typography.titleMedium,
// color = MaterialTheme.colorScheme.onSurface
// )
// ActivityItem(
// title = "Activity Report Generated",
// time = "2024-04-12 14:30",
// icon = Icons.Default.Description
// )
// ActivityItem(
// title = "Data Imported",
// time = "2024-04-12 10:15",
// icon = Icons.Default.Upload
// )
// }
// }
}
@Composable

View File

@ -11,6 +11,7 @@
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
C2894CEC2DB7FC08006301A9 /* BottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2894CEB2DB7FC08006301A9 /* BottomBar.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -21,6 +22,7 @@
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
C2894CEB2DB7FC08006301A9 /* BottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomBar.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -74,6 +76,7 @@
7555FF82242A565900829871 /* ContentView.swift */,
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iOSApp.swift */,
C2894CEB2DB7FC08006301A9 /* BottomBar.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
);
path = iosApp;
@ -186,6 +189,7 @@
files = (
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
C2894CEC2DB7FC08006301A9 /* BottomBar.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,192 @@
import SwiftUI
import Shared
struct BottomBar: View {
// Use meaningful tab names
@State private var selectedTab: Tab = .today
// Define Tabs with associated icons and names
enum Tab: CaseIterable {
case today, weekly, settings
var title: String {
switch self {
case .today: return "Today"
case .weekly: return "Weekly"
case .settings: return "Settings"
}
}
var iconName: String {
switch self {
case .today: return "chart.bar.xaxis"
case .weekly: return "calendar.day.timeline.leading"
case .settings: return "gear"
}
}
}
var body: some View {
TabView(selection: $selectedTab) {
// Use ContentView for the "Today" tab
ContentView()
.tag(Tab.today)
.tabItem { Label(Tab.today.title, systemImage: Tab.today.iconName) }
// Placeholder for Weekly view
WeeklyView()
.tag(Tab.weekly)
.tabItem { Label(Tab.weekly.title, systemImage: Tab.weekly.iconName) }
// Placeholder for Settings view
SettingsView()
.tag(Tab.settings)
.tabItem { Label(Tab.settings.title, systemImage: Tab.settings.iconName) }
}
.accentColor(.purple) // Example: Set a custom accent color for the tab bar
}
}
// --- Weekly View ---
struct WeeklyView: View {
// Placeholder data for weekly usage (e.g., hours per day)
let weeklyData: [Double] = [3.5, 4.2, 5.1, 2.8, 6.0, 7.5, 4.8]
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
let goalHours: Double = 8.0 // Example daily goal
// Calculate max value for chart scaling
var maxValue: Double {
(weeklyData.max() ?? goalHours) * 1.1 // Add 10% buffer
}
var body: some View {
NavigationView {
// Use List for standard iOS layout
List {
Section("Usage Trend") {
// Bar Chart Visualization
HStack(alignment: .bottom, spacing: 15) { // Increased spacing
ForEach(0..<weeklyData.count, id: \.self) { index in
VStack(spacing: 4) { // Spacing within the bar stack
Text(String(format: "%.1fh", weeklyData[index]))
.font(.caption)
.foregroundColor(.accentColor)
.lineLimit(1)
.rotationEffect(.degrees(-90)) // Rotate text for better fit if needed
.offset(y: -25) // Adjust offset if rotated
.frame(height: 50) // Give space for text
Rectangle()
.fill(weeklyData[index] > goalHours ? Color.orange : Color.accentColor) // Highlight days over goal
// Scale height relative to the max value
.frame(height: max(1, CGFloat(weeklyData[index] / maxValue) * 150)) // Ensure min height, scale based on max
.cornerRadius(5) // Rounded corners for bars
Text(days[index])
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity) // Allow bars to take equal width
}
}
.frame(height: 220) // Adjust overall chart height
.padding(.vertical) // Add padding around the chart
}
Section("Summary") {
HStack {
Text("Weekly Average")
Spacer()
Text(calculateWeeklyAverage())
.foregroundColor(.secondary)
}
HStack {
Text("Daily Goal")
Spacer()
Text(String(format: "%.1fh", goalHours))
.foregroundColor(.secondary)
}
HStack {
Text("Days Over Goal")
Spacer()
Text("\(daysOverGoalCount)")
.foregroundColor(.secondary)
}
}
}
.listStyle(.insetGrouped) // Use inset grouped style
.navigationTitle("Weekly Stats")
}
.navigationViewStyle(StackNavigationViewStyle())
}
// Helper function to calculate weekly average
func calculateWeeklyAverage() -> String {
let totalHours = weeklyData.reduce(0, +)
let averageHours = totalHours / Double(weeklyData.count)
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .abbreviated
// Convert hours to seconds for formatter
return formatter.string(from: TimeInterval(averageHours * 3600)) ?? "0m"
}
// Helper property for days over goal
var daysOverGoalCount: Int {
weeklyData.filter { $0 > goalHours }.count
}
}
// --- Settings View ---
struct SettingsView: View {
@State private var usageReminders = true
@State private var darkModeEnabled = false // Example state
@State private var selectedLimit = "No Limit"
let appLimits = ["No Limit", "1 hour", "2 hours", "Custom"]
var body: some View {
NavigationView {
Form {
Section("Notifications") {
Toggle("Usage Reminders", isOn: $usageReminders)
Toggle("Goal Achievement Alerts", isOn: .constant(false)) // Added toggle
}
Section("Usage Limits") {
Picker("Daily Limit", selection: $selectedLimit) {
ForEach(appLimits, id: \.self) {
Text($0)
}
}
NavigationLink("App Specific Limits", destination: Text("App Limits Detail (Placeholder)")) // Added link
}
Section("Appearance") {
Toggle("Dark Mode", isOn: $darkModeEnabled) // Added toggle
NavigationLink("Accent Color", destination: Text("Color Picker (Placeholder)"))
}
Section("About") {
HStack {
Text("Version")
Spacer()
Text("1.0.0")
.foregroundColor(.gray)
}
NavigationLink("Privacy Policy", destination: Text("Privacy Policy Details (Placeholder)"))
}
Section("test") {
Text("Hello From Kotlin Multiplatform: \n\(Greeting().greet()) ")
}
}
.navigationTitle("Settings")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct BottomBar_Previews: PreviewProvider {
static var previews: some View {
BottomBar()
}
}

View File

@ -1,33 +1,132 @@
import SwiftUI
import Shared
struct ContentView: View {
@State private var showContent = false
struct ContentView: View { // This now represents the "Today" view
// Placeholder goal for visualization
let dailyGoal: Double = 8 * 60 * 60 // 8 hours in seconds
// Placeholder current usage in seconds
let currentUsage: Double = 4.5 * 60 * 60 // 4.5 hours
// Placeholder App Usage Data
let appUsageData: [(name: String, time: String, icon: String, percentage: Double)] = [
("Social Media App", "1h 15m", "message.fill", 0.28),
("Video Streaming", "55m", "play.tv.fill", 0.20),
("Game", "40m", "gamecontroller.fill", 0.15),
("Browser", "30m", "safari.fill", 0.11),
("Music", "25m", "music.note", 0.09),
("News Reader", "15m", "newspaper.fill", 0.06)
]
var body: some View {
VStack {
Button("Click me!") {
withAnimation {
showContent = !showContent
}
}
NavigationView {
// Use List for standard iOS table view appearance
List {
// Section for the Progress Ring
Section {
VStack(spacing: 10) { // Adjusted spacing
Text("Total Usage Today")
.font(.title2)
.fontWeight(.semibold) // Slightly bolder title
.frame(maxWidth: .infinity, alignment: .center) // Center align
if showContent {
VStack(spacing: 16) {
Image(systemName: "swift")
.font(.system(size: 200))
// Progress Ring Visualization
ZStack {
Circle()
.stroke(lineWidth: 20) // Slightly thicker ring
.opacity(0.1)
.foregroundColor(.gray)
Circle()
.trim(from: 0.0, to: min(currentUsage / dailyGoal, 1.0))
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round)) // Thicker ring
.foregroundColor(.accentColor)
Text("SwiftUI: \(Greeting().greet())")
}
.transition(.move(edge: .top).combined(with: .opacity))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: currentUsage)
VStack {
Text(formattedTime(seconds: currentUsage))
.font(.largeTitle)
.fontWeight(.bold)
Text("of 8h Goal")
.font(.caption)
.foregroundColor(.secondary) // Use secondary color
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding()
.frame(width: 180, height: 180) // Slightly larger ring
.padding(.vertical) // Add vertical padding
}
.listRowInsets(EdgeInsets()) // Remove default insets for this row
.listRowBackground(Color.clear) // Make background clear if needed
}
// Section for Most Used Apps
Section("Most Used Apps") {
ForEach(appUsageData, id: \.name) { app in
AppUsageRow(
appName: app.name,
usageTime: app.time,
iconName: app.icon,
usagePercentage: app.percentage
)
}
}
}
.listStyle(.insetGrouped) // Use inset grouped style for modern look
.navigationTitle("Today's Usage")
// No need for explicit background color, List handles it
}
.navigationViewStyle(StackNavigationViewStyle())
}
// Helper to format seconds into hours/minutes
func formattedTime(seconds: Double) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .abbreviated
return formatter.string(from: TimeInterval(seconds)) ?? "0m"
}
}
// Helper view for displaying app usage rows
struct AppUsageRow: View {
let appName: String
let usageTime: String
let iconName: String
let usagePercentage: Double
var body: some View {
HStack(spacing: 15) { // Add spacing between elements
Image(systemName: iconName)
.font(.title3) // Slightly larger icon
.foregroundColor(.white)
.frame(width: 36, height: 36) // Slightly larger frame
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 8)) // Use rounded rectangle for icon background
VStack(alignment: .leading, spacing: 2) { // Reduced spacing in text stack
Text(appName)
.font(.body)
ProgressView(value: usagePercentage)
.progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
.frame(height: 5) // Slightly thicker bar
}
Spacer() // Pushes time to the right
Text(usageTime)
.font(.body) // Match body font
.foregroundColor(.secondary) // Use secondary color for less emphasis
}
.padding(.vertical, 6) // Adjust vertical padding within the row
}
}
// Keep the preview, it now shows the "Today" screen UI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark) // Preview in dark mode too
ContentView()
.preferredColorScheme(.light)
}
}

View File

@ -4,7 +4,7 @@ import SwiftUI
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
BottomBar()
}
}
}