openuispec 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -34
- package/cli/index.ts +1 -1
- package/docs/stress-test-maturity-report.md +97 -0
- package/examples/taskflow/screens/profile_edit.yaml +2 -0
- package/examples/todo-orbit/AGENTS.md +127 -0
- package/examples/todo-orbit/CLAUDE.md +127 -0
- package/examples/todo-orbit/README.md +62 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
- package/examples/todo-orbit/openuispec/README.md +158 -0
- package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
- package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
- package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
- package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
- package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
- package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
- package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
- package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/locales/en.json +150 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
- package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
- package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
- package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
- package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/screens/analytics.yaml +139 -0
- package/examples/todo-orbit/openuispec/screens/home.yaml +172 -0
- package/examples/todo-orbit/openuispec/screens/settings.yaml +148 -0
- package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
- package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
- package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
- package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
- package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
- package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
- package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
- package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
- package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
- package/package.json +1 -1
- package/schema/validate.ts +271 -4
- package/spec/openuispec-v0.1.md +80 -13
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package uz.rsteam.todoorbit
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material3.MaterialTheme
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.saveable.Saver
|
|
6
|
+
import androidx.compose.runtime.saveable.listSaver
|
|
7
|
+
import androidx.compose.ui.res.stringResource
|
|
8
|
+
import java.time.LocalDate
|
|
9
|
+
import java.time.LocalDateTime
|
|
10
|
+
|
|
11
|
+
enum class ScreenTab { Tasks, Analytics, Settings }
|
|
12
|
+
|
|
13
|
+
enum class UiLocale { En, Ru }
|
|
14
|
+
|
|
15
|
+
enum class ThemeMode { Light, Dark }
|
|
16
|
+
|
|
17
|
+
enum class TaskStatus { Open, Done }
|
|
18
|
+
|
|
19
|
+
enum class Priority { Low, Medium, High }
|
|
20
|
+
|
|
21
|
+
enum class TaskFilter { All, Open, Done }
|
|
22
|
+
|
|
23
|
+
enum class AnalyticsPeriod { Week, Month, Quarter }
|
|
24
|
+
|
|
25
|
+
enum class Cadence { Daily, Weekly, Monthly }
|
|
26
|
+
|
|
27
|
+
enum class Weekday { Mon, Tue, Wed, Thu, Fri, Sat, Sun }
|
|
28
|
+
|
|
29
|
+
enum class SummaryChannel { Push, Email }
|
|
30
|
+
|
|
31
|
+
enum class PreviewMode { Invalid, Empty, Ready }
|
|
32
|
+
|
|
33
|
+
sealed interface OverlayState {
|
|
34
|
+
data object CreateTask : OverlayState
|
|
35
|
+
data class EditTask(val taskId: String) : OverlayState
|
|
36
|
+
data object RecurringRule : OverlayState
|
|
37
|
+
data class TaskMeta(val taskId: String) : OverlayState
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
data class PreferencesState(
|
|
41
|
+
val locale: UiLocale = UiLocale.En,
|
|
42
|
+
val themeMode: ThemeMode = ThemeMode.Light,
|
|
43
|
+
val remindersEnabled: Boolean = true,
|
|
44
|
+
val dailySummaryEnabled: Boolean = false
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
val PreferencesStateSaver: Saver<PreferencesState, Any> = listSaver(
|
|
48
|
+
save = {
|
|
49
|
+
listOf(
|
|
50
|
+
it.locale.name,
|
|
51
|
+
it.themeMode.name,
|
|
52
|
+
it.remindersEnabled,
|
|
53
|
+
it.dailySummaryEnabled
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
restore = {
|
|
57
|
+
PreferencesState(
|
|
58
|
+
locale = UiLocale.valueOf(it[0] as String),
|
|
59
|
+
themeMode = ThemeMode.valueOf(it[1] as String),
|
|
60
|
+
remindersEnabled = it[2] as Boolean,
|
|
61
|
+
dailySummaryEnabled = it[3] as Boolean
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
data class TaskModel(
|
|
67
|
+
val id: String,
|
|
68
|
+
val title: String,
|
|
69
|
+
val notes: String = "",
|
|
70
|
+
val status: TaskStatus,
|
|
71
|
+
val priority: Priority,
|
|
72
|
+
val dueDate: LocalDate? = null,
|
|
73
|
+
val createdAt: LocalDateTime,
|
|
74
|
+
val updatedAt: LocalDateTime
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
data class RecurringRuleModel(
|
|
78
|
+
val id: String,
|
|
79
|
+
val name: String,
|
|
80
|
+
val cadence: Cadence,
|
|
81
|
+
val interval: Int,
|
|
82
|
+
val weekday: Weekday? = null,
|
|
83
|
+
val monthDay: Int? = null,
|
|
84
|
+
val startDate: LocalDate,
|
|
85
|
+
val endDate: LocalDate? = null,
|
|
86
|
+
val remindAt: String? = null,
|
|
87
|
+
val summaryChannel: SummaryChannel? = null
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
data class TrendPoint(
|
|
91
|
+
val label: String,
|
|
92
|
+
val completed: Int,
|
|
93
|
+
val created: Int
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
data class TaskDraft(
|
|
97
|
+
val title: String = "",
|
|
98
|
+
val notes: String = "",
|
|
99
|
+
val priority: Priority = Priority.Medium,
|
|
100
|
+
val dueDate: LocalDate? = null
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
data class RuleDraft(
|
|
104
|
+
val name: String = "",
|
|
105
|
+
val confirmName: String = "",
|
|
106
|
+
val cadence: Cadence? = null,
|
|
107
|
+
val interval: String = "1",
|
|
108
|
+
val weekday: Weekday? = null,
|
|
109
|
+
val monthDay: String = "",
|
|
110
|
+
val startDate: LocalDate = LocalDate.now(),
|
|
111
|
+
val hasEndDate: Boolean = false,
|
|
112
|
+
val endDate: LocalDate? = null,
|
|
113
|
+
val remindAt: String = "",
|
|
114
|
+
val enableSummary: Boolean = false,
|
|
115
|
+
val summaryChannel: SummaryChannel? = null
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
data class SchedulePreviewState(
|
|
119
|
+
val mode: PreviewMode,
|
|
120
|
+
val occurrences: List<LocalDate> = emptyList()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
sealed interface UiText {
|
|
124
|
+
data class Resource(
|
|
125
|
+
val id: Int,
|
|
126
|
+
val args: List<Any> = emptyList()
|
|
127
|
+
) : UiText
|
|
128
|
+
|
|
129
|
+
data class Literal(val value: String) : UiText
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
val TaskStatus.labelRes: Int
|
|
133
|
+
get() = when (this) {
|
|
134
|
+
TaskStatus.Open -> R.string.status_open
|
|
135
|
+
TaskStatus.Done -> R.string.status_done
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val Priority.labelRes: Int
|
|
139
|
+
get() = when (this) {
|
|
140
|
+
Priority.Low -> R.string.priority_low
|
|
141
|
+
Priority.Medium -> R.string.priority_medium
|
|
142
|
+
Priority.High -> R.string.priority_high
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
val Weekday.labelRes: Int
|
|
146
|
+
get() = when (this) {
|
|
147
|
+
Weekday.Mon -> R.string.weekday_mon
|
|
148
|
+
Weekday.Tue -> R.string.weekday_tue
|
|
149
|
+
Weekday.Wed -> R.string.weekday_wed
|
|
150
|
+
Weekday.Thu -> R.string.weekday_thu
|
|
151
|
+
Weekday.Fri -> R.string.weekday_fri
|
|
152
|
+
Weekday.Sat -> R.string.weekday_sat
|
|
153
|
+
Weekday.Sun -> R.string.weekday_sun
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
val Priority.color
|
|
157
|
+
@Composable get() = when (this) {
|
|
158
|
+
Priority.Low -> MaterialTheme.colorScheme.outline
|
|
159
|
+
Priority.Medium -> MaterialTheme.colorScheme.tertiary
|
|
160
|
+
Priority.High -> MaterialTheme.colorScheme.secondary
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Composable
|
|
164
|
+
fun UiText.resolve(): String {
|
|
165
|
+
return when (this) {
|
|
166
|
+
is UiText.Resource -> stringResource(id, *args.toTypedArray())
|
|
167
|
+
is UiText.Literal -> value
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package uz.rsteam.todoorbit
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.Column
|
|
7
|
+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
8
|
+
import androidx.compose.foundation.layout.FlowRow
|
|
9
|
+
import androidx.compose.foundation.layout.Row
|
|
10
|
+
import androidx.compose.foundation.layout.Spacer
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.padding
|
|
13
|
+
import androidx.compose.foundation.layout.size
|
|
14
|
+
import androidx.compose.foundation.layout.width
|
|
15
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
16
|
+
import androidx.compose.material.icons.Icons
|
|
17
|
+
import androidx.compose.material.icons.outlined.Translate
|
|
18
|
+
import androidx.compose.material3.Card
|
|
19
|
+
import androidx.compose.material3.CardDefaults
|
|
20
|
+
import androidx.compose.material3.ElevatedCard
|
|
21
|
+
import androidx.compose.material3.FilterChip
|
|
22
|
+
import androidx.compose.material3.Icon
|
|
23
|
+
import androidx.compose.material3.MaterialTheme
|
|
24
|
+
import androidx.compose.material3.OutlinedCard
|
|
25
|
+
import androidx.compose.material3.OutlinedTextField
|
|
26
|
+
import androidx.compose.material3.Switch
|
|
27
|
+
import androidx.compose.material3.Text
|
|
28
|
+
import androidx.compose.runtime.Composable
|
|
29
|
+
import androidx.compose.ui.Alignment
|
|
30
|
+
import androidx.compose.ui.Modifier
|
|
31
|
+
import androidx.compose.ui.graphics.Color
|
|
32
|
+
import androidx.compose.ui.res.stringResource
|
|
33
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
34
|
+
import androidx.compose.ui.unit.dp
|
|
35
|
+
|
|
36
|
+
@Composable
|
|
37
|
+
fun HeroCard(title: String, subtitle: String) {
|
|
38
|
+
ElevatedCard(
|
|
39
|
+
shape = MaterialTheme.shapes.large,
|
|
40
|
+
colors = CardDefaults.elevatedCardColors(
|
|
41
|
+
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f)
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
44
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
45
|
+
Text(title, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
|
46
|
+
Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Composable
|
|
52
|
+
fun StatCard(title: String, value: String) {
|
|
53
|
+
Card(shape = MaterialTheme.shapes.medium) {
|
|
54
|
+
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
55
|
+
Text(title, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
56
|
+
Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Composable
|
|
62
|
+
fun LegendDot(color: Color, label: String) {
|
|
63
|
+
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
64
|
+
Box(modifier = Modifier.size(10.dp).background(color, CircleShape))
|
|
65
|
+
Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Composable
|
|
70
|
+
fun LanguageSelector(current: UiLocale, onSelected: (UiLocale) -> Unit) {
|
|
71
|
+
EnumSelector(
|
|
72
|
+
title = stringResource(R.string.settings_language),
|
|
73
|
+
current = current.name,
|
|
74
|
+
options = listOf(
|
|
75
|
+
UiLocale.En.name to stringResource(R.string.settings_language_en),
|
|
76
|
+
UiLocale.Ru.name to stringResource(R.string.settings_language_ru)
|
|
77
|
+
),
|
|
78
|
+
leadingIcon = {
|
|
79
|
+
Icon(Icons.Outlined.Translate, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
80
|
+
},
|
|
81
|
+
onSelected = { selected -> onSelected(UiLocale.entries.first { it.name == selected }) }
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Composable
|
|
86
|
+
fun ThemeSelector(current: ThemeMode, onSelected: (ThemeMode) -> Unit) {
|
|
87
|
+
EnumSelector(
|
|
88
|
+
title = stringResource(R.string.settings_theme),
|
|
89
|
+
current = current.name,
|
|
90
|
+
options = listOf(
|
|
91
|
+
ThemeMode.Light.name to stringResource(R.string.settings_theme_light),
|
|
92
|
+
ThemeMode.Dark.name to stringResource(R.string.settings_theme_dark)
|
|
93
|
+
),
|
|
94
|
+
onSelected = { selected -> onSelected(ThemeMode.entries.first { it.name == selected }) }
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@OptIn(ExperimentalLayoutApi::class)
|
|
99
|
+
@Composable
|
|
100
|
+
fun EnumSelector(
|
|
101
|
+
title: String,
|
|
102
|
+
current: String,
|
|
103
|
+
options: List<Pair<String, String>>,
|
|
104
|
+
leadingIcon: @Composable (() -> Unit)? = null,
|
|
105
|
+
onSelected: (String) -> Unit
|
|
106
|
+
) {
|
|
107
|
+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
108
|
+
Text(title, style = MaterialTheme.typography.titleSmall)
|
|
109
|
+
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
110
|
+
options.forEach { (value, label) ->
|
|
111
|
+
FilterChip(
|
|
112
|
+
selected = value == current,
|
|
113
|
+
onClick = { onSelected(value) },
|
|
114
|
+
label = { Text(label) },
|
|
115
|
+
leadingIcon = if (value == current && leadingIcon != null) leadingIcon else null
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Composable
|
|
123
|
+
fun SettingsToggle(
|
|
124
|
+
title: String,
|
|
125
|
+
subtitle: String,
|
|
126
|
+
checked: Boolean,
|
|
127
|
+
onCheckedChange: (Boolean) -> Unit
|
|
128
|
+
) {
|
|
129
|
+
Row(
|
|
130
|
+
modifier = Modifier.fillMaxWidth(),
|
|
131
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
132
|
+
verticalAlignment = Alignment.CenterVertically
|
|
133
|
+
) {
|
|
134
|
+
Column(modifier = Modifier.fillMaxWidth(0.8f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
135
|
+
Text(title, fontWeight = FontWeight.SemiBold)
|
|
136
|
+
Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
137
|
+
}
|
|
138
|
+
Spacer(Modifier.width(12.dp))
|
|
139
|
+
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Composable
|
|
144
|
+
fun RuleTextField(
|
|
145
|
+
label: String,
|
|
146
|
+
value: String,
|
|
147
|
+
placeholder: String = "",
|
|
148
|
+
helper: String? = null,
|
|
149
|
+
error: String? = null,
|
|
150
|
+
onValueChange: (String) -> Unit
|
|
151
|
+
) {
|
|
152
|
+
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
153
|
+
OutlinedTextField(
|
|
154
|
+
value = value,
|
|
155
|
+
onValueChange = onValueChange,
|
|
156
|
+
modifier = Modifier.fillMaxWidth(),
|
|
157
|
+
label = { Text(label) },
|
|
158
|
+
placeholder = if (placeholder.isNotBlank()) ({ Text(placeholder) }) else null,
|
|
159
|
+
shape = MaterialTheme.shapes.medium,
|
|
160
|
+
isError = error != null
|
|
161
|
+
)
|
|
162
|
+
when {
|
|
163
|
+
error != null -> Text(error, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
|
164
|
+
helper != null -> Text(helper, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Composable
|
|
170
|
+
fun EmptyCard(title: String, body: String) {
|
|
171
|
+
OutlinedCard(shape = MaterialTheme.shapes.large) {
|
|
172
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
173
|
+
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
|
174
|
+
Text(body, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Composable
|
|
180
|
+
fun LabelValue(label: String, value: String) {
|
|
181
|
+
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
182
|
+
Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
183
|
+
Text(value, fontWeight = FontWeight.SemiBold)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
package uz.rsteam.todoorbit
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Canvas
|
|
4
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
5
|
+
import androidx.compose.foundation.layout.Column
|
|
6
|
+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
7
|
+
import androidx.compose.foundation.layout.FlowRow
|
|
8
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
9
|
+
import androidx.compose.foundation.layout.Row
|
|
10
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.height
|
|
13
|
+
import androidx.compose.foundation.layout.padding
|
|
14
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
15
|
+
import androidx.compose.foundation.lazy.grid.GridCells
|
|
16
|
+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
17
|
+
import androidx.compose.material3.ElevatedCard
|
|
18
|
+
import androidx.compose.material3.FilterChip
|
|
19
|
+
import androidx.compose.material3.MaterialTheme
|
|
20
|
+
import androidx.compose.material3.Text
|
|
21
|
+
import androidx.compose.runtime.Composable
|
|
22
|
+
import androidx.compose.ui.Modifier
|
|
23
|
+
import androidx.compose.ui.geometry.Offset
|
|
24
|
+
import androidx.compose.ui.graphics.Path
|
|
25
|
+
import androidx.compose.ui.graphics.StrokeCap
|
|
26
|
+
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
27
|
+
import androidx.compose.ui.res.stringResource
|
|
28
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
29
|
+
import androidx.compose.ui.unit.dp
|
|
30
|
+
|
|
31
|
+
@OptIn(ExperimentalLayoutApi::class)
|
|
32
|
+
@Composable
|
|
33
|
+
fun AnalyticsScreen(
|
|
34
|
+
period: AnalyticsPeriod,
|
|
35
|
+
overview: AnalyticsOverview,
|
|
36
|
+
trendSeries: List<TrendPoint>,
|
|
37
|
+
overdueTasks: List<TaskModel>,
|
|
38
|
+
onPeriodChange: (AnalyticsPeriod) -> Unit,
|
|
39
|
+
locale: UiLocale
|
|
40
|
+
) {
|
|
41
|
+
LazyColumn(
|
|
42
|
+
modifier = Modifier.fillMaxSize(),
|
|
43
|
+
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
44
|
+
contentPadding = PaddingValues(20.dp)
|
|
45
|
+
) {
|
|
46
|
+
item {
|
|
47
|
+
HeroCard(
|
|
48
|
+
stringResource(R.string.analytics_title),
|
|
49
|
+
stringResource(R.string.analytics_subtitle)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
item {
|
|
53
|
+
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
54
|
+
AnalyticsPeriod.entries.forEach { entry ->
|
|
55
|
+
FilterChip(
|
|
56
|
+
selected = period == entry,
|
|
57
|
+
onClick = { onPeriodChange(entry) },
|
|
58
|
+
label = {
|
|
59
|
+
Text(
|
|
60
|
+
when (entry) {
|
|
61
|
+
AnalyticsPeriod.Week -> stringResource(R.string.analytics_period_week)
|
|
62
|
+
AnalyticsPeriod.Month -> stringResource(R.string.analytics_period_month)
|
|
63
|
+
AnalyticsPeriod.Quarter -> stringResource(R.string.analytics_period_quarter)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
item {
|
|
72
|
+
LazyVerticalGrid(
|
|
73
|
+
columns = GridCells.Fixed(2),
|
|
74
|
+
modifier = Modifier.height(220.dp),
|
|
75
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
76
|
+
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
77
|
+
userScrollEnabled = false
|
|
78
|
+
) {
|
|
79
|
+
item { StatCard(stringResource(R.string.analytics_completed_today), overview.completedToday.toString()) }
|
|
80
|
+
item { StatCard(stringResource(R.string.analytics_open_tasks), overview.openTasks.toString()) }
|
|
81
|
+
item { StatCard(stringResource(R.string.analytics_overdue_tasks), overview.overdueTasks.toString()) }
|
|
82
|
+
item { StatCard(stringResource(R.string.analytics_completion_rate), "${overview.completionRate}%") }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
item {
|
|
86
|
+
TrendChartCard(series = trendSeries, period = period)
|
|
87
|
+
}
|
|
88
|
+
item {
|
|
89
|
+
ElevatedCard(shape = MaterialTheme.shapes.large) {
|
|
90
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
91
|
+
Text(stringResource(R.string.analytics_overdue_section), style = MaterialTheme.typography.headlineSmall)
|
|
92
|
+
Text(stringResource(R.string.analytics_overdue_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
93
|
+
if (overdueTasks.isEmpty()) {
|
|
94
|
+
Text(stringResource(R.string.analytics_empty_overdue_body), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
95
|
+
} else {
|
|
96
|
+
overdueTasks.forEach { task ->
|
|
97
|
+
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
98
|
+
Column {
|
|
99
|
+
Text(task.title, fontWeight = FontWeight.SemiBold)
|
|
100
|
+
Text(stringResource(task.priority.labelRes), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
101
|
+
}
|
|
102
|
+
Text(task.dueDate?.let { formatAbsolute(it, locale) }.orEmpty())
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Composable
|
|
113
|
+
fun TrendChartCard(
|
|
114
|
+
series: List<TrendPoint>,
|
|
115
|
+
period: AnalyticsPeriod
|
|
116
|
+
) {
|
|
117
|
+
val outlineVariant = MaterialTheme.colorScheme.outlineVariant
|
|
118
|
+
val createdColor = MaterialTheme.colorScheme.secondary
|
|
119
|
+
val completedColor = MaterialTheme.colorScheme.primary
|
|
120
|
+
|
|
121
|
+
ElevatedCard(shape = MaterialTheme.shapes.large) {
|
|
122
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
123
|
+
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
|
124
|
+
Column {
|
|
125
|
+
Text(
|
|
126
|
+
stringResource(R.string.analytics_contract_label),
|
|
127
|
+
style = MaterialTheme.typography.labelSmall,
|
|
128
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
129
|
+
)
|
|
130
|
+
Text(
|
|
131
|
+
when (period) {
|
|
132
|
+
AnalyticsPeriod.Week -> stringResource(R.string.analytics_period_week)
|
|
133
|
+
AnalyticsPeriod.Month -> stringResource(R.string.analytics_period_month)
|
|
134
|
+
AnalyticsPeriod.Quarter -> stringResource(R.string.analytics_period_quarter)
|
|
135
|
+
},
|
|
136
|
+
style = MaterialTheme.typography.titleLarge
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
140
|
+
LegendDot(color = createdColor, label = stringResource(R.string.analytics_legend_created))
|
|
141
|
+
LegendDot(color = completedColor, label = stringResource(R.string.analytics_legend_completed))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (series.isEmpty()) {
|
|
145
|
+
Text(stringResource(R.string.analytics_empty_trend), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
146
|
+
} else {
|
|
147
|
+
Canvas(modifier = Modifier.fillMaxWidth().height(220.dp)) {
|
|
148
|
+
val maxValue = series.maxOf { maxOf(it.completed, it.created) }.coerceAtLeast(1)
|
|
149
|
+
val leftPad = 32.dp.toPx()
|
|
150
|
+
val bottomPad = 28.dp.toPx()
|
|
151
|
+
val usableWidth = size.width - leftPad * 2
|
|
152
|
+
val usableHeight = size.height - bottomPad - 20.dp.toPx()
|
|
153
|
+
repeat(4) { index ->
|
|
154
|
+
val y = 20.dp.toPx() + (usableHeight / 3f) * index
|
|
155
|
+
drawLine(
|
|
156
|
+
color = outlineVariant,
|
|
157
|
+
start = Offset(leftPad, y),
|
|
158
|
+
end = Offset(size.width - leftPad, y)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
fun buildPath(selector: (TrendPoint) -> Int): Path {
|
|
162
|
+
return Path().apply {
|
|
163
|
+
series.forEachIndexed { index, point ->
|
|
164
|
+
val x = leftPad + (usableWidth / series.lastIndex.coerceAtLeast(1)) * index
|
|
165
|
+
val y = 20.dp.toPx() + usableHeight - (usableHeight * selector(point) / maxValue.toFloat())
|
|
166
|
+
if (index == 0) moveTo(x, y) else lineTo(x, y)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
drawPath(
|
|
171
|
+
path = buildPath { it.created },
|
|
172
|
+
color = createdColor,
|
|
173
|
+
style = Stroke(width = 6f, cap = StrokeCap.Round)
|
|
174
|
+
)
|
|
175
|
+
drawPath(
|
|
176
|
+
path = buildPath { it.completed },
|
|
177
|
+
color = completedColor,
|
|
178
|
+
style = Stroke(width = 6f, cap = StrokeCap.Round)
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
|
182
|
+
series.forEach { point ->
|
|
183
|
+
Text(
|
|
184
|
+
text = point.label,
|
|
185
|
+
style = MaterialTheme.typography.labelSmall,
|
|
186
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package uz.rsteam.todoorbit
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
6
|
+
import androidx.compose.foundation.layout.Spacer
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
|
+
import androidx.compose.foundation.layout.padding
|
|
9
|
+
import androidx.compose.foundation.layout.width
|
|
10
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
11
|
+
import androidx.compose.material.icons.Icons
|
|
12
|
+
import androidx.compose.material.icons.outlined.CalendarMonth
|
|
13
|
+
import androidx.compose.material3.Button
|
|
14
|
+
import androidx.compose.material3.ElevatedCard
|
|
15
|
+
import androidx.compose.material3.Icon
|
|
16
|
+
import androidx.compose.material3.MaterialTheme
|
|
17
|
+
import androidx.compose.material3.OutlinedCard
|
|
18
|
+
import androidx.compose.material3.Text
|
|
19
|
+
import androidx.compose.runtime.Composable
|
|
20
|
+
import androidx.compose.ui.Modifier
|
|
21
|
+
import androidx.compose.ui.res.stringResource
|
|
22
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
23
|
+
import androidx.compose.ui.unit.dp
|
|
24
|
+
|
|
25
|
+
@Composable
|
|
26
|
+
fun SettingsScreen(
|
|
27
|
+
preferences: PreferencesState,
|
|
28
|
+
recurringRules: List<RecurringRuleModel>,
|
|
29
|
+
onPreferencesChange: (PreferencesState) -> Unit,
|
|
30
|
+
onOpenRecurringRule: () -> Unit,
|
|
31
|
+
locale: UiLocale
|
|
32
|
+
) {
|
|
33
|
+
LazyColumn(
|
|
34
|
+
modifier = Modifier.fillMaxSize(),
|
|
35
|
+
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
36
|
+
contentPadding = PaddingValues(20.dp)
|
|
37
|
+
) {
|
|
38
|
+
item {
|
|
39
|
+
HeroCard(
|
|
40
|
+
stringResource(R.string.settings_title),
|
|
41
|
+
stringResource(R.string.settings_subtitle)
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
item {
|
|
45
|
+
ElevatedCard(shape = MaterialTheme.shapes.large) {
|
|
46
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
47
|
+
LanguageSelector(
|
|
48
|
+
current = preferences.locale,
|
|
49
|
+
onSelected = { onPreferencesChange(preferences.copy(locale = it)) }
|
|
50
|
+
)
|
|
51
|
+
ThemeSelector(
|
|
52
|
+
current = preferences.themeMode,
|
|
53
|
+
onSelected = { onPreferencesChange(preferences.copy(themeMode = it)) }
|
|
54
|
+
)
|
|
55
|
+
SettingsToggle(
|
|
56
|
+
title = stringResource(R.string.settings_reminders),
|
|
57
|
+
subtitle = stringResource(R.string.settings_reminders_helper),
|
|
58
|
+
checked = preferences.remindersEnabled,
|
|
59
|
+
onCheckedChange = { onPreferencesChange(preferences.copy(remindersEnabled = it)) }
|
|
60
|
+
)
|
|
61
|
+
SettingsToggle(
|
|
62
|
+
title = stringResource(R.string.settings_daily_summary),
|
|
63
|
+
subtitle = stringResource(R.string.settings_daily_summary_helper),
|
|
64
|
+
checked = preferences.dailySummaryEnabled,
|
|
65
|
+
onCheckedChange = { onPreferencesChange(preferences.copy(dailySummaryEnabled = it)) }
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
item {
|
|
71
|
+
ElevatedCard(shape = MaterialTheme.shapes.large) {
|
|
72
|
+
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
73
|
+
Text(stringResource(R.string.settings_automation_title), style = MaterialTheme.typography.titleLarge)
|
|
74
|
+
Text(stringResource(R.string.settings_automation_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
75
|
+
Button(onClick = onOpenRecurringRule, shape = MaterialTheme.shapes.medium) {
|
|
76
|
+
Icon(Icons.Outlined.CalendarMonth, contentDescription = null)
|
|
77
|
+
Spacer(Modifier.width(8.dp))
|
|
78
|
+
Text(stringResource(R.string.settings_automation_create_rule))
|
|
79
|
+
}
|
|
80
|
+
recurringRules.forEach { rule ->
|
|
81
|
+
OutlinedCard(shape = MaterialTheme.shapes.medium) {
|
|
82
|
+
Column(modifier = Modifier.padding(16.dp)) {
|
|
83
|
+
Text(rule.name, fontWeight = FontWeight.SemiBold)
|
|
84
|
+
Text(ruleDescription(rule, locale), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Composable
|
|
95
|
+
private fun ruleDescription(rule: RecurringRuleModel, locale: UiLocale): String {
|
|
96
|
+
val cadence = when (rule.cadence) {
|
|
97
|
+
Cadence.Daily -> stringResource(R.string.recurring_rule_cadence_daily)
|
|
98
|
+
Cadence.Weekly -> "${stringResource(R.string.recurring_rule_cadence_weekly)} · ${stringResource(rule.weekday!!.labelRes)}"
|
|
99
|
+
Cadence.Monthly -> "${stringResource(R.string.recurring_rule_cadence_monthly)} · ${rule.monthDay}"
|
|
100
|
+
}
|
|
101
|
+
return "$cadence · ${formatAbsolute(rule.startDate, locale)}"
|
|
102
|
+
}
|