├── .gitignore
├── .idea
├── .gitignore
├── AndroidProjectSystem.xml
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── binissa
│ │ └── calendar
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── binissa
│ │ │ └── calendar
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── calendar
│ │ │ ├── CalendarContract.kt
│ │ │ ├── CalendarCoordinator.kt
│ │ │ ├── CalendarRoute.kt
│ │ │ ├── CalendarScreen.kt
│ │ │ ├── CalendarViewModel.kt
│ │ │ ├── TaskItem.kt
│ │ │ └── components
│ │ │ │ ├── AnimatedCalendarHeader.kt
│ │ │ │ ├── AnimatedTaskItem.kt
│ │ │ │ ├── BlurTextReveal.kt
│ │ │ │ ├── SwipeToDeleteContainer.kt
│ │ │ │ ├── TaskDialog.kt
│ │ │ │ ├── TaskItemCard.kt
│ │ │ │ ├── TaskTypeButton.kt
│ │ │ │ ├── WeekCalendar.kt
│ │ │ │ └── morph
│ │ │ │ ├── CharacterTransition.kt
│ │ │ │ ├── DayNameMorph.kt
│ │ │ │ ├── DirectionalTextTransition.kt
│ │ │ │ ├── DirectionalTransitionConfig.kt
│ │ │ │ └── TransitionControlPanel.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── binissa
│ └── calendar
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://github.com/user-attachments/assets/502cff83-03ba-4ca4-b4b3-b1ca4b135311
4 |
5 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.compose)
5 | alias(libs.plugins.ksp)
6 | alias(libs.plugins.hilt)
7 | }
8 |
9 | android {
10 | namespace = "com.binissa.calendar"
11 | compileSdk = 35
12 |
13 | defaultConfig {
14 | applicationId = "com.binissa.calendar"
15 | minSdk = 24
16 | targetSdk = 35
17 | versionCode = 1
18 | versionName = "1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_11
34 | targetCompatibility = JavaVersion.VERSION_11
35 | }
36 | kotlinOptions {
37 | jvmTarget = "11"
38 | }
39 | buildFeatures {
40 | compose = true
41 | }
42 | }
43 |
44 | dependencies {
45 |
46 | implementation(libs.androidx.core.ktx)
47 | implementation(libs.androidx.lifecycle.runtime.ktx)
48 | implementation(libs.androidx.activity.compose)
49 | implementation(platform(libs.androidx.compose.bom))
50 | implementation(libs.androidx.ui)
51 | implementation(libs.androidx.ui.graphics)
52 | implementation(libs.androidx.ui.tooling.preview)
53 | implementation(libs.androidx.material3)
54 | testImplementation(libs.junit)
55 | androidTestImplementation(libs.androidx.junit)
56 | androidTestImplementation(libs.androidx.espresso.core)
57 | androidTestImplementation(platform(libs.androidx.compose.bom))
58 | androidTestImplementation(libs.androidx.ui.test.junit4)
59 | debugImplementation(libs.androidx.ui.tooling)
60 | debugImplementation(libs.androidx.ui.test.manifest)
61 |
62 |
63 | //Hilt
64 | implementation (libs.hilt.android)
65 | implementation (libs.androidx.hilt.navigation.compose)
66 |
67 | ksp(libs.dagger.compiler)
68 | ksp(libs.hilt.compiler)
69 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/binissa/calendar/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.binissa.calendar", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/App.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class App: Application() {
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import com.binissa.calendar.calendar.CalendarRoute
16 | import com.binissa.calendar.calendar.components.morph.DirectionalTextTransition
17 | import com.binissa.calendar.calendar.components.morph.DirectionalTransitionConfig
18 | import com.binissa.calendar.calendar.components.morph.TransitionControlPanel
19 | import com.binissa.calendar.ui.theme.CalendarTheme
20 | import dagger.hilt.android.AndroidEntryPoint
21 |
22 | @AndroidEntryPoint
23 | class MainActivity : ComponentActivity() {
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | enableEdgeToEdge()
27 | setContent {
28 | CalendarTheme {
29 | var currentConfig by remember { mutableStateOf(DirectionalTransitionConfig()) }
30 |
31 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
32 | TransitionControlPanel(
33 | modifier = Modifier
34 | .padding(innerPadding)
35 | .fillMaxSize(),
36 | onConfigChanged = { currentConfig = it }
37 | )
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/CalendarContract.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import java.time.LocalDate
4 |
5 | /**
6 | * UI State that represents CalendarScreen
7 | **/
8 | data class CalendarState(
9 | val selectedDate: LocalDate = LocalDate.now(),
10 | val tasks: List = emptyList(),
11 | val isAddingTask: Boolean = false,
12 | val newTaskTitle: String = "",
13 | val editingTask: TaskItem? = null,
14 | val selectedDateEvents: Boolean = false
15 | )
16 |
17 | /**
18 | * Calendar Actions emitted from the UI Layer
19 | * passed to the coordinator to handle
20 | **/
21 | data class CalendarActions(
22 | val onDateSelected: (LocalDate) -> Unit = {},
23 | val onTaskCompleteToggle: (TaskItem) -> Unit = {},
24 | val onAddTaskClick: () -> Unit = {},
25 | val onDismissAddTask: () -> Unit = {},
26 | val onNewTaskTitleChange: (String) -> Unit = {},
27 | val onSaveNewTask: (TaskType) -> Unit = {},
28 | val onTaskClick: (TaskItem) -> Unit = {},
29 | val onDeleteTask: (TaskItem) -> Unit = {},
30 | val onSwipeToNextDay: () -> Unit = {},
31 | val onSwipeToPreviousDay: () -> Unit = {}
32 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/CalendarCoordinator.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import java.time.LocalDate
7 |
8 | /**
9 | * Screen's coordinator which is responsible for handling actions from the UI layer
10 | * and one-shot actions based on the new UI state
11 | */
12 | class CalendarCoordinator(
13 | val viewModel: CalendarViewModel
14 | ) {
15 | val screenStateFlow = viewModel.stateFlow
16 |
17 | fun onDateSelected(date: LocalDate) {
18 | viewModel.selectDate(date)
19 | }
20 |
21 | fun onTaskCompleteToggle(task: TaskItem) {
22 | viewModel.toggleTaskCompleted(task)
23 | }
24 |
25 | fun onAddTaskClick() {
26 | viewModel.showAddTask()
27 | }
28 |
29 | fun onDismissAddTask() {
30 | viewModel.dismissAddTask()
31 | }
32 |
33 | fun onNewTaskTitleChange(title: String) {
34 | viewModel.updateNewTaskTitle(title)
35 | }
36 |
37 | fun onSaveNewTask(type: TaskType) {
38 | viewModel.saveNewTask(type)
39 | }
40 |
41 | fun onTaskClick(task: TaskItem) {
42 | viewModel.editTask(task)
43 | }
44 |
45 | fun onDeleteTask(task: TaskItem) {
46 | viewModel.deleteTask(task)
47 | }
48 |
49 | fun onSwipeToNextDay() {
50 | viewModel.navigateToNextDay()
51 | }
52 |
53 | fun onSwipeToPreviousDay() {
54 | viewModel.navigateToPreviousDay()
55 | }
56 | }
57 |
58 | @Composable
59 | fun rememberCalendarCoordinator(
60 | viewModel: CalendarViewModel = hiltViewModel()
61 | ): CalendarCoordinator {
62 | return remember(viewModel) {
63 | CalendarCoordinator(
64 | viewModel = viewModel
65 | )
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/CalendarRoute.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.remember
7 |
8 |
9 | @Composable
10 | fun CalendarRoute(
11 | coordinator: CalendarCoordinator = rememberCalendarCoordinator()
12 | ) {
13 | // State observing and declarations
14 | val uiState by coordinator.screenStateFlow.collectAsState(CalendarState())
15 |
16 | // UI Actions
17 | val actions = rememberCalendarActions(coordinator)
18 |
19 | // UI Rendering
20 | CalendarScreen(uiState, actions)
21 | }
22 |
23 |
24 | @Composable
25 | fun rememberCalendarActions(coordinator: CalendarCoordinator): CalendarActions {
26 | return remember(coordinator) {
27 | CalendarActions(
28 | onDateSelected = coordinator::onDateSelected,
29 | onTaskCompleteToggle = coordinator::onTaskCompleteToggle,
30 | onAddTaskClick = coordinator::onAddTaskClick,
31 | onDismissAddTask = coordinator::onDismissAddTask,
32 | onNewTaskTitleChange = coordinator::onNewTaskTitleChange,
33 | onSaveNewTask = coordinator::onSaveNewTask,
34 | onTaskClick = coordinator::onTaskClick,
35 | onDeleteTask = coordinator::onDeleteTask,
36 | onSwipeToNextDay = coordinator::onSwipeToNextDay,
37 | onSwipeToPreviousDay = coordinator::onSwipeToPreviousDay
38 | )
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/CalendarScreen.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import androidx.compose.animation.animateColorAsState
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.border
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.gestures.detectHorizontalDragGestures
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.PaddingValues
15 | import androidx.compose.foundation.layout.Row
16 | import androidx.compose.foundation.layout.Spacer
17 | import androidx.compose.foundation.layout.aspectRatio
18 | import androidx.compose.foundation.layout.fillMaxSize
19 | import androidx.compose.foundation.layout.fillMaxWidth
20 | import androidx.compose.foundation.layout.height
21 | import androidx.compose.foundation.layout.padding
22 | import androidx.compose.foundation.lazy.LazyColumn
23 | import androidx.compose.foundation.lazy.grid.GridCells
24 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
25 | import androidx.compose.foundation.lazy.grid.items
26 | import androidx.compose.foundation.lazy.items
27 | import androidx.compose.foundation.shape.CircleShape
28 | import androidx.compose.foundation.shape.RoundedCornerShape
29 | import androidx.compose.material.icons.Icons
30 | import androidx.compose.material.icons.filled.Add
31 | import androidx.compose.material3.Card
32 | import androidx.compose.material3.CardDefaults
33 | import androidx.compose.material3.FloatingActionButton
34 | import androidx.compose.material3.Icon
35 | import androidx.compose.material3.MaterialTheme
36 | import androidx.compose.material3.Scaffold
37 | import androidx.compose.material3.Surface
38 | import androidx.compose.material3.Text
39 | import androidx.compose.material3.TextButton
40 | import androidx.compose.runtime.Composable
41 | import androidx.compose.runtime.getValue
42 | import androidx.compose.runtime.mutableFloatStateOf
43 | import androidx.compose.runtime.mutableStateOf
44 | import androidx.compose.runtime.remember
45 | import androidx.compose.runtime.setValue
46 | import androidx.compose.ui.Alignment
47 | import androidx.compose.ui.Modifier
48 | import androidx.compose.ui.draw.clip
49 | import androidx.compose.ui.graphics.Brush
50 | import androidx.compose.ui.graphics.Color
51 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
52 | import androidx.compose.ui.input.pointer.pointerInput
53 | import androidx.compose.ui.platform.LocalHapticFeedback
54 | import androidx.compose.ui.text.font.FontWeight
55 | import androidx.compose.ui.text.style.TextAlign
56 | import androidx.compose.ui.tooling.preview.Preview
57 | import androidx.compose.ui.unit.dp
58 | import com.binissa.calendar.calendar.components.AnimatedTaskItem
59 | import com.binissa.calendar.calendar.components.SwipeToDeleteContainer
60 | import com.binissa.calendar.calendar.components.TaskDialog
61 | import com.binissa.calendar.calendar.components.WeekCalendar
62 | import java.time.DayOfWeek
63 | import java.time.LocalDate
64 | import java.time.YearMonth
65 | import java.time.format.TextStyle
66 | import java.time.temporal.ChronoUnit
67 | import java.time.temporal.TemporalAdjusters
68 | import java.util.Locale
69 |
70 | @RequiresApi(Build.VERSION_CODES.O)
71 | @Composable
72 | fun CalendarScreen(
73 | state: CalendarState, actions: CalendarActions
74 | ) {
75 | val haptic = LocalHapticFeedback.current
76 | var dragOffset by remember { mutableFloatStateOf(0f) }
77 |
78 | Scaffold(
79 | floatingActionButton = {
80 | FloatingActionButton(
81 | onClick = { actions.onAddTaskClick() }, containerColor = Color(0xFFFF5252)
82 | ) {
83 | Icon(
84 | imageVector = Icons.Default.Add,
85 | contentDescription = "Add Task",
86 | tint = Color.White
87 | )
88 | }
89 | }) { paddingValues ->
90 | Surface(
91 | modifier = Modifier
92 | .fillMaxSize()
93 | .padding(paddingValues)
94 | .pointerInput(Unit) {
95 | detectHorizontalDragGestures(onDragEnd = {
96 | if (dragOffset > 100) {
97 | actions.onSwipeToPreviousDay()
98 | haptic.performHapticFeedback(HapticFeedbackType.LongPress)
99 | } else if (dragOffset < -100) {
100 | actions.onSwipeToNextDay()
101 | haptic.performHapticFeedback(HapticFeedbackType.LongPress)
102 | }
103 | dragOffset = 0f
104 | }, onDragCancel = { dragOffset = 0f }, onHorizontalDrag = { _, dragAmount ->
105 | dragOffset += dragAmount
106 | })
107 | }, color = MaterialTheme.colorScheme.background
108 | ) {
109 | LazyColumn(
110 | modifier = Modifier
111 | .fillMaxSize()
112 | .padding(16.dp),
113 | contentPadding = PaddingValues(bottom = 80.dp)
114 | ) {
115 | item {
116 | AnimatedCalendarHeader(
117 | selectedDate = state.selectedDate,
118 | onPreviousDayClick = actions.onSwipeToPreviousDay,
119 | onNextDayClick = actions.onSwipeToNextDay
120 | )
121 | }
122 |
123 | item {
124 | Spacer(modifier = Modifier.height(24.dp))
125 | WeekCalendar(
126 | selectedDate = state.selectedDate, onDateSelected = actions.onDateSelected
127 | )
128 | Spacer(modifier = Modifier.height(24.dp))
129 | }
130 |
131 | if (state.tasks.isEmpty()) {
132 | item {
133 | Box(
134 | modifier = Modifier
135 | .fillMaxWidth()
136 | .padding(32.dp),
137 | contentAlignment = Alignment.Center
138 | ) {
139 | Text(
140 | text = "No tasks for this day",
141 | color = Color.Gray,
142 | style = MaterialTheme.typography.bodyLarge
143 | )
144 | }
145 | }
146 | } else {
147 | items(state.tasks) { taskItem ->
148 | SwipeToDeleteContainer(
149 | onDelete = { actions.onDeleteTask(taskItem) }) {
150 | AnimatedTaskItem(
151 | task = taskItem,
152 | onClick = { actions.onTaskClick(taskItem) },
153 | onCheckboxClick = { actions.onTaskCompleteToggle(taskItem) })
154 | }
155 |
156 | Spacer(modifier = Modifier.height(8.dp))
157 | }
158 | }
159 | }
160 |
161 | // Add/Edit Task Dialog
162 | if (state.isAddingTask) {
163 | TaskDialog(
164 | taskTitle = state.newTaskTitle,
165 | isEditing = state.editingTask != null,
166 | onDismiss = actions.onDismissAddTask,
167 | onTitleChange = actions.onNewTaskTitleChange,
168 | onSave = actions.onSaveNewTask
169 | )
170 | }
171 | }
172 | }
173 | }
174 |
175 |
176 | @RequiresApi(Build.VERSION_CODES.O)
177 | @Composable
178 | @Preview(name = "Calendar")
179 | private fun CalendarScreenPreview() {
180 | MaterialTheme {
181 | CalendarScreen(
182 | state = CalendarState(
183 | selectedDate = LocalDate.now(), tasks = listOf(
184 | TaskItem(id = "1", title = "Daria's 20th Birthday", type = TaskType.BIRTHDAY),
185 | TaskItem(id = "2", title = "Wake up", time = "09:00", type = TaskType.STAR),
186 | TaskItem(
187 | id = "3", title = "Design Crit", time = "10:00", type = TaskType.CHECKBOX
188 | )
189 | )
190 | ), actions = CalendarActions()
191 | )
192 | }
193 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/CalendarViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import java.time.LocalDate
8 | import javax.inject.Inject
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 | import java.util.UUID
15 |
16 | @HiltViewModel
17 | class CalendarViewModel @Inject constructor(
18 | savedStateHandle: SavedStateHandle
19 | ) : ViewModel() {
20 |
21 | private val _stateFlow: MutableStateFlow = MutableStateFlow(CalendarState(
22 | selectedDate = LocalDate.now(),
23 | tasks = getSampleTaskItems()
24 | ))
25 |
26 | val stateFlow: StateFlow = _stateFlow.asStateFlow()
27 |
28 | fun selectDate(date: LocalDate) {
29 | _stateFlow.update { currentState ->
30 | currentState.copy(
31 | selectedDate = date,
32 | // In a real app, you would fetch tasks for this date from a repository
33 | tasks = getSampleTaskItems().filter {
34 | LocalDate.now().dayOfMonth == date.dayOfMonth
35 | }
36 | )
37 | }
38 | }
39 |
40 | fun toggleTaskCompleted(task: TaskItem) {
41 | _stateFlow.update { currentState ->
42 | val updatedTasks = currentState.tasks.map {
43 | if (it.id == task.id) it.copy(isCompleted = !it.isCompleted) else it
44 | }
45 | currentState.copy(tasks = updatedTasks)
46 | }
47 | }
48 |
49 | fun showAddTask() {
50 | _stateFlow.update { it.copy(isAddingTask = true, newTaskTitle = "", editingTask = null) }
51 | }
52 |
53 | fun dismissAddTask() {
54 | _stateFlow.update { it.copy(isAddingTask = false, newTaskTitle = "", editingTask = null) }
55 | }
56 |
57 | fun updateNewTaskTitle(title: String) {
58 | _stateFlow.update { it.copy(newTaskTitle = title) }
59 | }
60 |
61 | fun saveNewTask(type: TaskType) {
62 | val currentState = _stateFlow.value
63 |
64 | if (currentState.newTaskTitle.isBlank()) return
65 |
66 | val newTask = TaskItem(
67 | id = UUID.randomUUID().toString(),
68 | title = currentState.newTaskTitle,
69 | type = type,
70 | date = currentState.selectedDate
71 | )
72 |
73 | _stateFlow.update { state ->
74 | state.copy(
75 | tasks = state.tasks + newTask,
76 | isAddingTask = false,
77 | newTaskTitle = ""
78 | )
79 | }
80 | }
81 |
82 | fun editTask(task: TaskItem) {
83 | _stateFlow.update { it.copy(editingTask = task, isAddingTask = true, newTaskTitle = task.title) }
84 | }
85 |
86 | fun deleteTask(task: TaskItem) {
87 | _stateFlow.update { currentState ->
88 | currentState.copy(tasks = currentState.tasks.filter { it.id != task.id })
89 | }
90 | }
91 |
92 | fun navigateToNextDay() {
93 | _stateFlow.update { currentState ->
94 | val nextDay = currentState.selectedDate.plusDays(1)
95 | currentState.copy(selectedDate = nextDay)
96 | }
97 | }
98 |
99 | fun navigateToPreviousDay() {
100 | _stateFlow.update { currentState ->
101 | val previousDay = currentState.selectedDate.minusDays(1)
102 | currentState.copy(selectedDate = previousDay)
103 | }
104 | }
105 |
106 | private fun getSampleTaskItems(): List {
107 | return listOf(
108 | TaskItem(id = "1", title = "Daria's 20th Birthday", type = TaskType.BIRTHDAY, date = LocalDate.now()),
109 | TaskItem(id = "2", title = "Wake up", time = "09:00", type = TaskType.STAR, date = LocalDate.now()),
110 | TaskItem(id = "3", title = "Design Crit", time = "10:00", type = TaskType.CHECKBOX, date = LocalDate.now()),
111 | TaskItem(id = "4", title = "Haircut with Vincent", time = "13:00", type = TaskType.CHECKBOX, date = LocalDate.now()),
112 | TaskItem(id = "5", title = "Make pasta", type = TaskType.CHECKBOX, date = LocalDate.now()),
113 | TaskItem(id = "6", title = "Pushups x100", type = TaskType.CHECKBOX, isCompleted = true, date = LocalDate.now()),
114 | TaskItem(id = "7", title = "Wind down", time = "21:00", type = TaskType.MOON, date = LocalDate.now())
115 | )
116 | }
117 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/TaskItem.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import java.time.LocalDate
4 |
5 | data class TaskItem(
6 | val id: String,
7 | val title: String,
8 | val time: String? = null,
9 | val type: TaskType = TaskType.CHECKBOX,
10 | val isCompleted: Boolean = false,
11 | val isHighlighted: Boolean = false,
12 | val date: LocalDate = LocalDate.now()
13 | )
14 |
15 | enum class TaskType {
16 | BIRTHDAY, STAR, CHECKBOX, MOON
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/AnimatedCalendarHeader.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import androidx.compose.foundation.BorderStroke
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.rememberScrollState
12 | import androidx.compose.foundation.verticalScroll
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
15 | import androidx.compose.material.icons.rounded.KeyboardArrowRight
16 | import androidx.compose.material3.Button
17 | import androidx.compose.material3.Card
18 | import androidx.compose.material3.CardDefaults
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.IconButton
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.Slider
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.LaunchedEffect
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.mutableStateOf
28 | import androidx.compose.runtime.remember
29 | import androidx.compose.runtime.setValue
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.unit.dp
35 | import com.binissa.calendar.calendar.components.morph.AnimatedDayName
36 | import com.binissa.calendar.calendar.components.morph.DirectionalTransitionConfig
37 | import java.time.LocalDate
38 | import java.time.format.DateTimeFormatter
39 |
40 | /**
41 | * A Material 3 styled calendar header with smooth transitioning animations.
42 | *
43 | * @param selectedDate The currently selected date
44 | * @param onPreviousDayClick Callback when navigating to previous day
45 | * @param onNextDayClick Callback when navigating to next day
46 | * @param accentColor The accent color for the current day indicator (defaults to primary)
47 | * @param modifier Modifier for the composable
48 | */
49 | @RequiresApi(Build.VERSION_CODES.O)
50 | @Composable
51 | fun AnimatedCalendarHeader(
52 | selectedDate: LocalDate,
53 | onPreviousDayClick: () -> Unit,
54 | onNextDayClick: () -> Unit,
55 | accentColor: Color = MaterialTheme.colorScheme.primary,
56 | modifier: Modifier = Modifier
57 | ) {
58 | // Animation state to coordinate date transitions
59 | val transitionConfig = remember {
60 | DirectionalTransitionConfig(
61 | transitionDuration = 650,
62 | staggerDelay = 80,
63 | staggerFactor = 1.4f,
64 | trailCount = 7,
65 | maxVerticalOffset = 10.dp,
66 | maxHorizontalOffset = 2.dp,
67 | maxBlur = 4.dp,
68 | rotationAmount = 4f,
69 | scaleRange = Pair(0.2f, 1.1f),
70 | bunching = 2.5f
71 | )
72 | }
73 |
74 | Row(
75 | modifier = modifier.padding(vertical = 8.dp),
76 | horizontalArrangement = Arrangement.SpaceBetween,
77 | verticalAlignment = Alignment.CenterVertically
78 | ) {
79 | Row(verticalAlignment = Alignment.CenterVertically) {
80 | IconButton(onClick = onPreviousDayClick) {
81 | Icon(
82 | imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
83 | contentDescription = "Previous Day",
84 | tint = MaterialTheme.colorScheme.onSurfaceVariant
85 | )
86 | }
87 |
88 | Row(
89 | verticalAlignment = Alignment.CenterVertically,
90 | horizontalArrangement = Arrangement.SpaceBetween
91 | ) {
92 | AnimatedDayName(
93 | date = selectedDate,
94 | config = transitionConfig,
95 | textStyle = MaterialTheme.typography.headlineLarge,
96 | color = MaterialTheme.colorScheme.onSurface
97 | )
98 | }
99 |
100 | IconButton(onClick = onNextDayClick) {
101 | Icon(
102 | imageVector = Icons.Rounded.KeyboardArrowRight,
103 | contentDescription = "Next Day",
104 | tint = MaterialTheme.colorScheme.onSurfaceVariant
105 | )
106 | }
107 | }
108 |
109 | // Year with subtle animation
110 | Text(
111 | text = selectedDate.format(DateTimeFormatter.ofPattern("yyyy")),
112 | style = MaterialTheme.typography.bodyLarge,
113 | color = MaterialTheme.colorScheme.onSurfaceVariant
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/AnimatedTaskItem.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.interaction.MutableInteractionSource
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Check
14 | import androidx.compose.material.icons.outlined.CheckCircle
15 | import androidx.compose.material.icons.rounded.Star
16 | import androidx.compose.material3.Card
17 | import androidx.compose.material3.CardDefaults
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import com.binissa.calendar.calendar.TaskItem
32 | import com.binissa.calendar.calendar.TaskType
33 |
34 | @Composable
35 | fun AnimatedTaskItem(
36 | task: TaskItem,
37 | onClick: () -> Unit,
38 | onCheckboxClick: () -> Unit,
39 | isVisible: Boolean = true,
40 | index: Int = 0
41 | ) {
42 | val animatedElevation by animateFloatAsState(
43 | targetValue = if (task.isCompleted) 0f else 4f, label = "cardElevation"
44 | )
45 |
46 | // Animation state
47 | val revealConfig = BlurTextRevealConfig(
48 | direction = RevealDirection.START_TO_END,
49 | initialBlur = 6f,
50 | initialOffset = 24f,
51 | blurDuration = 700,
52 | alphaDuration = 500
53 | )
54 |
55 | Card(
56 | modifier = Modifier
57 | .fillMaxWidth()
58 | .clickable(onClick = onClick),
59 | elevation = CardDefaults.cardElevation(defaultElevation = animatedElevation.dp),
60 | colors = CardDefaults.cardColors(
61 | containerColor = if (task.isCompleted) Color.LightGray.copy(alpha = 0.3f)
62 | else MaterialTheme.colorScheme.surface
63 | ),
64 | shape = RoundedCornerShape(12.dp)
65 | ) {
66 | Row(
67 | modifier = Modifier
68 | .fillMaxWidth()
69 | .padding(16.dp),
70 | verticalAlignment = Alignment.CenterVertically
71 | ) {
72 | // Icon based on task type with animated transitions
73 | when (task.type) {
74 | TaskType.BIRTHDAY -> {
75 | Text(
76 | text = "✹",
77 | color = Color(0xFFFF5252),
78 | fontSize = 20.sp,
79 | modifier = Modifier.padding(end = 16.dp)
80 | )
81 | }
82 |
83 | TaskType.STAR -> {
84 | Icon(
85 | imageVector = Icons.Rounded.Star,
86 | contentDescription = "Star",
87 | tint = Color(0xFFFFD700),
88 | modifier = Modifier.padding(end = 16.dp)
89 | )
90 | }
91 |
92 | TaskType.CHECKBOX -> {
93 | Box(
94 | modifier = Modifier
95 | .clip(CircleShape)
96 | .clickable(
97 | interactionSource = remember { MutableInteractionSource() },
98 | indication = null,
99 | onClick = onCheckboxClick
100 | )
101 | .padding(end = 16.dp)
102 | ) {
103 | if (task.isCompleted) {
104 | Icon(
105 | imageVector = Icons.Default.Check,
106 | contentDescription = "Completed",
107 | tint = Color.Gray
108 | )
109 | } else {
110 | Icon(
111 | imageVector = Icons.Outlined.CheckCircle,
112 | contentDescription = "Not completed",
113 | tint = Color.Gray
114 | )
115 | }
116 | }
117 | }
118 |
119 | TaskType.MOON -> {
120 | Text(
121 | text = "🌙",
122 | fontSize = 20.sp,
123 | color = Color(0xFF7E57C2),
124 | modifier = Modifier.padding(end = 16.dp)
125 | )
126 | }
127 | }
128 |
129 | // Task title with blur reveal animation
130 | BlurTextReveal(
131 | text = task.title,
132 | isTriggered = isVisible,
133 | color = if (task.isCompleted) Color.Gray else Color.Black,
134 | config = revealConfig,
135 | textStyle = MaterialTheme.typography.bodyLarge.copy(
136 | fontWeight = if (task.isHighlighted) FontWeight.Bold else FontWeight.Normal,
137 | fontSize = 16.sp
138 | ),
139 | staggerDelay = 15L,
140 | modifier = Modifier.weight(1f)
141 | )
142 |
143 | // Time if available
144 | if (task.time != null) {
145 | BlurTextReveal(
146 | text = task.time,
147 | isTriggered = isVisible,
148 | color = Color.Gray,
149 | config = revealConfig.copy(
150 | direction = RevealDirection.TOP_TO_BOTTOM, blurDuration = 600
151 | ),
152 | textStyle = MaterialTheme.typography.bodyMedium.copy(
153 | fontSize = 14.sp
154 | ),
155 | staggerDelay = 20L
156 | )
157 | }
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/BlurTextReveal.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 |
4 | import androidx.compose.animation.core.Animatable
5 | import androidx.compose.animation.core.FastOutSlowInEasing
6 | import androidx.compose.animation.core.Spring
7 | import androidx.compose.animation.core.spring
8 | import androidx.compose.animation.core.tween
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.offset
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.Stable
18 | import androidx.compose.runtime.key
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.BlurredEdgeTreatment
23 | import androidx.compose.ui.draw.blur
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.graphics.graphicsLayer
26 | import androidx.compose.ui.text.TextStyle
27 | import androidx.compose.ui.text.font.FontFamily
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.unit.dp
30 | import kotlinx.coroutines.delay
31 | import kotlinx.coroutines.launch
32 |
33 | enum class RevealDirection {
34 | BOTTOM_TO_TOP, TOP_TO_BOTTOM, START_TO_END, END_TO_START
35 | }
36 |
37 | @Stable
38 | data class BlurTextRevealConfig(
39 | val initialBlur: Float = 8f,
40 | val initialOffset: Float = 4f,
41 | val blurDuration: Int = 600,
42 | val alphaDuration: Int = 400,
43 | val glowBlur: Float = 16f,
44 | val glowAlpha: Float = 0.3f,
45 | val direction: RevealDirection = RevealDirection.BOTTOM_TO_TOP
46 | )
47 |
48 |
49 | @Composable
50 | private fun BlurCharReveal(
51 | char: Char,
52 | color: Color,
53 | delayMillis: Long,
54 | isTriggered: Boolean,
55 | config: BlurTextRevealConfig = BlurTextRevealConfig(),
56 | modifier: Modifier = Modifier,
57 | textStyle: TextStyle = MaterialTheme.typography.labelLarge,
58 | fontWeight: FontWeight? = null,
59 | fontFamily: FontFamily? = null,
60 | ) {
61 | val blur = remember { Animatable(config.initialBlur) }
62 | val alpha = remember { Animatable(0f) }
63 | val offsetX = remember { Animatable(0f) }
64 | val offsetY = remember { Animatable(0f) }
65 |
66 | // Reset animations when trigger is false
67 | LaunchedEffect(isTriggered) {
68 | if (!isTriggered) {
69 | blur.snapTo(config.initialBlur)
70 | alpha.snapTo(0f)
71 | when (config.direction) {
72 | RevealDirection.BOTTOM_TO_TOP -> offsetY.snapTo(config.initialOffset)
73 | RevealDirection.TOP_TO_BOTTOM -> offsetY.snapTo(-config.initialOffset)
74 | RevealDirection.START_TO_END -> offsetX.snapTo(-config.initialOffset)
75 | RevealDirection.END_TO_START -> offsetX.snapTo(config.initialOffset)
76 | }
77 | }
78 | }
79 |
80 | // Start animations when triggered
81 | LaunchedEffect(isTriggered) {
82 | if (isTriggered) {
83 | delay(delayMillis)
84 |
85 | launch {
86 | alpha.animateTo(
87 | targetValue = 1f, animationSpec = tween(
88 | durationMillis = config.alphaDuration, easing = FastOutSlowInEasing
89 | )
90 | )
91 | }
92 |
93 | launch {
94 | blur.animateTo(
95 | targetValue = 0f, animationSpec = tween(
96 | durationMillis = config.blurDuration, easing = FastOutSlowInEasing
97 | )
98 | )
99 | }
100 |
101 | launch {
102 | offsetX.animateTo(
103 | targetValue = 0f, animationSpec = spring(
104 | dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow
105 | )
106 | )
107 | }
108 |
109 | launch {
110 | offsetY.animateTo(
111 | targetValue = 0f, animationSpec = spring(
112 | dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow
113 | )
114 | )
115 | }
116 | }
117 | }
118 |
119 | Box {
120 | // Glow effect
121 | Text(text = char.toString(),
122 | style = textStyle,
123 | color = color.copy(alpha = config.glowAlpha),
124 | modifier = modifier
125 | .graphicsLayer {
126 | this.alpha = alpha.value
127 | }
128 | .offset(x = offsetX.value.dp, y = offsetY.value.dp)
129 | .blur(
130 | config.glowBlur.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded
131 | ))
132 |
133 | // Main character
134 | Text(text = char.toString(),
135 | style = textStyle,
136 | color = color,
137 | fontWeight = fontWeight,
138 | fontFamily = fontFamily,
139 | modifier = modifier
140 | .graphicsLayer {
141 | this.alpha = alpha.value
142 | }
143 | .offset(x = offsetX.value.dp, y = offsetY.value.dp)
144 | .blur(
145 | blur.value.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded
146 | ))
147 | }
148 | }
149 |
150 | @Composable
151 | fun BlurTextReveal(
152 | text: String,
153 | color: Color = Color.Unspecified,
154 | isTriggered: Boolean,
155 | modifier: Modifier = Modifier,
156 | config: BlurTextRevealConfig = BlurTextRevealConfig(),
157 | staggerDelay: Long = 50L,
158 | textStyle: TextStyle = MaterialTheme.typography.displayLarge,
159 | fontWeight: FontWeight? = null,
160 | fontFamily: FontFamily? = null,
161 | ) {
162 | Row(
163 | modifier = modifier,
164 | horizontalArrangement = Arrangement.Start,
165 | verticalAlignment = Alignment.CenterVertically
166 | ) {
167 | text.forEachIndexed { index, char ->
168 | key(index) {
169 | BlurCharReveal(
170 | char = char,
171 | color = color,
172 | delayMillis = index * staggerDelay,
173 | isTriggered = isTriggered,
174 | config = config,
175 | textStyle = textStyle,
176 | fontWeight = fontWeight,
177 | fontFamily = fontFamily
178 | )
179 | }
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/SwipeToDeleteContainer.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.gestures.Orientation
5 | import androidx.compose.foundation.gestures.draggable
6 | import androidx.compose.foundation.gestures.rememberDraggableState
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.offset
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.filled.Delete
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.runtime.mutableFloatStateOf
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.setValue
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.draw.clip
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
26 | import androidx.compose.ui.platform.LocalDensity
27 | import androidx.compose.ui.platform.LocalHapticFeedback
28 | import androidx.compose.ui.unit.IntOffset
29 | import androidx.compose.ui.unit.dp
30 | import kotlin.math.roundToInt
31 |
32 | @Composable
33 | fun SwipeToDeleteContainer(
34 | onDelete: () -> Unit, content: @Composable () -> Unit
35 | ) {
36 | val width = 200.dp
37 | val widthPx = with(LocalDensity.current) { width.toPx() }
38 | var offsetX by remember { mutableFloatStateOf(0f) }
39 | val haptic = LocalHapticFeedback.current
40 |
41 | Box(
42 | modifier = Modifier.fillMaxWidth()
43 | ) {
44 | // Delete background
45 | Box(
46 | modifier = Modifier
47 | .fillMaxWidth()
48 | .height(56.dp)
49 | .clip(RoundedCornerShape(12.dp))
50 | .background(Color.Red.copy(alpha = 0.8f)), contentAlignment = Alignment.CenterEnd
51 | ) {
52 | Icon(
53 | imageVector = Icons.Default.Delete,
54 | contentDescription = "Delete",
55 | tint = Color.White,
56 | modifier = Modifier.padding(end = 16.dp)
57 | )
58 | }
59 |
60 | // Foreground content (draggable)
61 | Box(modifier = Modifier
62 | .offset { IntOffset(offsetX.roundToInt(), 0) }
63 | .draggable(
64 | orientation = Orientation.Horizontal,
65 | state = rememberDraggableState { delta ->
66 | offsetX = (offsetX + delta).coerceAtMost(0f)
67 | },
68 | onDragStopped = {
69 | if (offsetX < -widthPx * 0.5f) {
70 | haptic.performHapticFeedback(HapticFeedbackType.LongPress)
71 | onDelete()
72 | offsetX = 0f
73 | } else {
74 | offsetX = 0f
75 | }
76 | })) {
77 | content()
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/TaskDialog.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material3.Card
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.OutlinedTextField
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TextButton
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.window.Dialog
23 | import com.binissa.calendar.calendar.TaskType
24 |
25 | @Composable
26 | fun TaskDialog(
27 | taskTitle: String,
28 | isEditing: Boolean,
29 | onDismiss: () -> Unit,
30 | onTitleChange: (String) -> Unit,
31 | onSave: (TaskType) -> Unit
32 | ) {
33 | Dialog(onDismissRequest = onDismiss) {
34 | Card(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .padding(16.dp),
38 | shape = RoundedCornerShape(16.dp)
39 | ) {
40 | Column(
41 | modifier = Modifier
42 | .fillMaxWidth()
43 | .padding(20.dp),
44 | horizontalAlignment = Alignment.CenterHorizontally
45 | ) {
46 | Text(
47 | text = if (isEditing) "Edit Task" else "Add New Task",
48 | style = MaterialTheme.typography.headlineSmall,
49 | fontWeight = FontWeight.Bold
50 | )
51 |
52 | Spacer(modifier = Modifier.height(16.dp))
53 |
54 | OutlinedTextField(
55 | value = taskTitle,
56 | onValueChange = onTitleChange,
57 | label = { Text("Task Title") },
58 | modifier = Modifier.fillMaxWidth()
59 | )
60 |
61 | Spacer(modifier = Modifier.height(16.dp))
62 |
63 | Text(
64 | text = "Task Type",
65 | style = MaterialTheme.typography.bodyLarge
66 | )
67 |
68 | Spacer(modifier = Modifier.height(8.dp))
69 |
70 | Row(
71 | modifier = Modifier.fillMaxWidth(),
72 | horizontalArrangement = Arrangement.SpaceEvenly
73 | ) {
74 | TaskTypeButton(
75 | text = "✹",
76 | color = Color(0xFFFF5252),
77 | onClick = { onSave(TaskType.BIRTHDAY) }
78 | )
79 |
80 | TaskTypeButton(
81 | text = "★",
82 | color = Color(0xFFFFD700),
83 | onClick = { onSave(TaskType.STAR) }
84 | )
85 |
86 | TaskTypeButton(
87 | text = "☐",
88 | color = Color.Gray,
89 | onClick = { onSave(TaskType.CHECKBOX) }
90 | )
91 |
92 | TaskTypeButton(
93 | text = "🌙",
94 | color = Color(0xFF7E57C2),
95 | onClick = { onSave(TaskType.MOON) }
96 | )
97 | }
98 |
99 | Spacer(modifier = Modifier.height(16.dp))
100 |
101 | Row(
102 | modifier = Modifier.fillMaxWidth(),
103 | horizontalArrangement = Arrangement.End
104 | ) {
105 | TextButton(onClick = onDismiss) {
106 | Text("Cancel")
107 | }
108 | }
109 | }
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/TaskItemCard.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.interaction.MutableInteractionSource
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Check
14 | import androidx.compose.material.icons.outlined.CheckCircle
15 | import androidx.compose.material.icons.rounded.Star
16 | import androidx.compose.material3.Card
17 | import androidx.compose.material3.CardDefaults
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import com.binissa.calendar.calendar.TaskItem
32 | import com.binissa.calendar.calendar.TaskType
33 |
34 | @Composable
35 | fun TaskItemCard(
36 | task: TaskItem, onClick: () -> Unit, onCheckboxClick: () -> Unit
37 | ) {
38 | val animatedElevation by animateFloatAsState(
39 | targetValue = if (task.isCompleted) 0f else 4f, label = "cardElevation"
40 | )
41 |
42 | Card(
43 | modifier = Modifier
44 | .fillMaxWidth()
45 | .clickable(onClick = onClick),
46 | elevation = CardDefaults.cardElevation(defaultElevation = animatedElevation.dp),
47 | colors = CardDefaults.cardColors(
48 | containerColor = if (task.isCompleted) Color.LightGray.copy(alpha = 0.3f)
49 | else MaterialTheme.colorScheme.surface
50 | ),
51 | shape = RoundedCornerShape(12.dp)
52 | ) {
53 | Row(
54 | modifier = Modifier
55 | .fillMaxWidth()
56 | .padding(16.dp),
57 | verticalAlignment = Alignment.CenterVertically
58 | ) {
59 | // Icon based on task type with animated transitions
60 | when (task.type) {
61 | TaskType.BIRTHDAY -> {
62 | Text(
63 | text = "✹",
64 | color = Color(0xFFFF5252),
65 | fontSize = 20.sp,
66 | modifier = Modifier.padding(end = 16.dp)
67 | )
68 | }
69 |
70 | TaskType.STAR -> {
71 | Icon(
72 | imageVector = Icons.Rounded.Star,
73 | contentDescription = "Star",
74 | tint = Color(0xFFFFD700),
75 | modifier = Modifier.padding(end = 16.dp)
76 | )
77 | }
78 |
79 | TaskType.CHECKBOX -> {
80 | Box(
81 | modifier = Modifier
82 | .clip(CircleShape)
83 | .clickable(
84 | interactionSource = remember { MutableInteractionSource() },
85 | indication = null,
86 | onClick = onCheckboxClick
87 | )
88 | .padding(end = 16.dp)
89 | ) {
90 | if (task.isCompleted) {
91 | Icon(
92 | imageVector = Icons.Default.Check,
93 | contentDescription = "Completed",
94 | tint = Color.Gray
95 | )
96 | } else {
97 | Icon(
98 | imageVector = Icons.Outlined.CheckCircle,
99 | contentDescription = "Not completed",
100 | tint = Color.Gray
101 | )
102 | }
103 | }
104 | }
105 |
106 | TaskType.MOON -> {
107 | Text(
108 | text = "🌙",
109 | fontSize = 20.sp,
110 | color = Color(0xFF7E57C2),
111 | modifier = Modifier.padding(end = 16.dp)
112 | )
113 | }
114 | }
115 |
116 | // Task title
117 | Text(
118 | text = task.title,
119 | fontSize = 16.sp,
120 | color = if (task.isCompleted) Color.Gray else Color.Black,
121 | style = MaterialTheme.typography.bodyLarge.copy(
122 | fontWeight = if (task.isHighlighted) FontWeight.Bold else FontWeight.Normal
123 | ),
124 | modifier = Modifier.weight(1f)
125 | )
126 |
127 | // Time if available
128 | if (task.time != null) {
129 | Text(
130 | text = task.time, fontSize = 14.sp, color = Color.Gray
131 | )
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/TaskTypeButton.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.draw.clip
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import androidx.compose.ui.unit.sp
16 |
17 | @Composable
18 | fun TaskTypeButton(
19 | text: String,
20 | color: Color,
21 | onClick: () -> Unit
22 | ) {
23 | Box(
24 | modifier = Modifier
25 | .size(48.dp)
26 | .clip(CircleShape)
27 | .background(color.copy(alpha = 0.2f))
28 | .clickable(onClick = onClick),
29 | contentAlignment = Alignment.Center
30 | ) {
31 | Text(
32 | text = text,
33 | color = color,
34 | fontSize = 24.sp
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/WeekCalendar.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.shape.CircleShape
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import java.time.LocalDate
23 | import java.time.format.TextStyle
24 | import java.util.Locale
25 |
26 | @Composable
27 | fun WeekCalendar(
28 | selectedDate: LocalDate,
29 | onDateSelected: (LocalDate) -> Unit
30 | ) {
31 | // Calculate first day of week (Monday) for the week containing selectedDate
32 | val firstDayOfWeek = selectedDate.minusDays(selectedDate.dayOfWeek.value - 1L)
33 |
34 | Row(
35 | modifier = Modifier.fillMaxWidth(),
36 | horizontalArrangement = Arrangement.SpaceBetween
37 | ) {
38 | for (i in 0..6) {
39 | val date = firstDayOfWeek.plusDays(i.toLong())
40 | val isSelected = date.equals(selectedDate)
41 | val isToday = date.equals(LocalDate.now())
42 |
43 | Column(
44 | horizontalAlignment = Alignment.CenterHorizontally,
45 | modifier = Modifier
46 | .clip(CircleShape)
47 | .clickable { onDateSelected(date) }
48 | .padding(vertical = 4.dp)
49 | ) {
50 | Text(
51 | text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
52 | .take(3).uppercase(),
53 | fontSize = 12.sp,
54 | color = Color.Gray
55 | )
56 |
57 | Box(
58 | modifier = Modifier
59 | .padding(4.dp)
60 | .size(36.dp)
61 | .clip(CircleShape)
62 | .background(
63 | when {
64 | isSelected -> Color(0xFFFF5252)
65 | isToday -> Color(0xFFFF5252).copy(alpha = 0.3f)
66 | else -> Color.Transparent
67 | }
68 | ),
69 | contentAlignment = Alignment.Center
70 | ) {
71 | Text(
72 | text = date.dayOfMonth.toString(),
73 | fontSize = 14.sp,
74 | color = if (isSelected) Color.White else Color.Black,
75 | fontWeight = if (isSelected || isToday) FontWeight.Bold else FontWeight.Normal
76 | )
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/morph/CharacterTransition.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components.morph
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.CubicBezierEasing
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.offset
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.rememberCoroutineScope
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.BlurredEdgeTreatment
17 | import androidx.compose.ui.draw.alpha
18 | import androidx.compose.ui.draw.blur
19 | import androidx.compose.ui.draw.rotate
20 | import androidx.compose.ui.draw.scale
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.platform.LocalDensity
23 | import androidx.compose.ui.text.TextStyle
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.text.style.TextAlign
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import kotlinx.coroutines.delay
29 | import kotlinx.coroutines.launch
30 | import kotlin.math.pow
31 | import kotlin.math.sin
32 |
33 | /**
34 | * Animates a single character transition with ghost trail effect.
35 | */
36 | @Composable
37 | fun CharacterTransition(
38 | fromChar: Char,
39 | toChar: Char,
40 | isAnimating: Boolean,
41 | direction: TransitionDirection,
42 | config: DirectionalTransitionConfig,
43 | delayMillis: Long = 0,
44 | textStyle: TextStyle = MaterialTheme.typography.titleLarge.copy(
45 | fontWeight = FontWeight.Bold,
46 | fontSize = 32.sp,
47 | ),
48 | color: Color = Color.White,
49 | letterIndex: Int = 0,
50 | onComplete: () -> Unit = {}
51 | ) {
52 | // Animation state
53 | val progress = remember { Animatable(0f) }
54 | val scope = rememberCoroutineScope()
55 |
56 | // Direction multiplier for vertical movement
57 | val directionMultiplier = if (direction == TransitionDirection.FORWARD) 1f else -1f
58 |
59 | // Subtle horizontal movement (slightly different for each character)
60 | val horizontalSway = remember { (letterIndex % 2) * 2 - 1 } // Alternates between -1 and 1
61 |
62 | // Convert Dp values to pixels for calculations
63 | val maxVerticalOffsetPx = with(LocalDensity.current) { config.maxVerticalOffset.toPx() }
64 | val maxHorizontalOffsetPx = with(LocalDensity.current) {
65 | (config.maxHorizontalOffset.toPx() + (letterIndex % 3))
66 | }
67 | val maxBlurPx = with(LocalDensity.current) { config.maxBlur.toPx() }
68 |
69 | // Reset and trigger animations when isAnimating changes
70 | LaunchedEffect(isAnimating, fromChar, toChar) {
71 | if (isAnimating && fromChar != toChar) {
72 | progress.snapTo(0f)
73 | delay(delayMillis)
74 | scope.launch {
75 | progress.animateTo(
76 | targetValue = 1f, animationSpec = tween(
77 | durationMillis = config.transitionDuration, easing = config.easingCurve
78 | )
79 | )
80 | delay(100) // Small delay before signaling completion for visual polish
81 | onComplete()
82 | }
83 | }
84 | }
85 |
86 | // Enhanced easing curves for more dramatic animation
87 | val incomingEasing = CubicBezierEasing(0.34f, 1.56f, 0.64f, 1f) // Slight overshoot
88 | val outgoingEasing = CubicBezierEasing(0.36f, 0f, 0.66f, -0.56f) // Dramatic exit
89 |
90 | // Derived progress values
91 | val outgoingProgress = outgoingEasing.transform(progress.value)
92 | val incomingProgress = incomingEasing.transform(progress.value)
93 |
94 | Box(
95 | contentAlignment = Alignment.Center,
96 | ) {
97 | val (minScale, maxScale) = config.scaleRange
98 | val toCharScale = when {
99 | incomingProgress < 0.7f -> minScale + (incomingProgress * 1.3f * (1f - minScale))
100 | else -> maxScale - ((incomingProgress - 0.7f) * (maxScale - 1f) / 0.3f)
101 | }
102 |
103 | // Position with directional movement
104 | val toCharVerticalOffset =
105 | ((1f - incomingProgress) * maxVerticalOffsetPx * -directionMultiplier).dp
106 | val toCharHorizontalOffset =
107 | (sin(incomingProgress * Math.PI) * maxHorizontalOffsetPx * horizontalSway).dp
108 | val toCharRotation = (1f - incomingProgress) * directionMultiplier * -config.rotationAmount
109 |
110 | // Alpha with quick fade-in
111 | val toCharAlpha = when {
112 | incomingProgress < 0.3f -> incomingProgress * 3.33f
113 | else -> 1f
114 | }
115 |
116 | // Blur effect that clears quickly
117 | val toCharBlur = (maxBlurPx * (1f - incomingProgress).pow(2f)).dp
118 |
119 | // Render incoming character
120 | Text(
121 | text = toChar.toString(),
122 | style = textStyle,
123 | color = color,
124 | textAlign = TextAlign.Center,
125 | modifier = Modifier
126 | .offset(x = toCharHorizontalOffset, y = toCharVerticalOffset)
127 | .alpha(toCharAlpha)
128 | .blur(toCharBlur, BlurredEdgeTreatment.Unbounded)
129 | .scale(toCharScale)
130 | .rotate(toCharRotation)
131 | )
132 |
133 | // OUTGOING CHARACTER ANIMATION (MAIN)
134 | // -------------------------------
135 | val fromCharMainAlpha = (1f - outgoingProgress.pow(0.7f)).coerceIn(0f, 1f)
136 | val fromCharMainOffset =
137 | (outgoingProgress.pow(1.2f) * maxVerticalOffsetPx * directionMultiplier).dp
138 | val fromCharMainHOffset =
139 | (sin(outgoingProgress * Math.PI) * -maxHorizontalOffsetPx * horizontalSway).dp
140 | val fromCharMainBlur = (outgoingProgress.pow(0.8f) * maxBlurPx * 0.6f).dp
141 |
142 | // Scaling effect
143 | val fromCharMainScale = when {
144 | outgoingProgress < 0.2f -> 1f + (outgoingProgress * 0.1f)
145 | else -> 1.02f - ((outgoingProgress - 0.2f) * 0.3f)
146 | }
147 |
148 | // Rotation effect
149 | val fromCharMainRotation = outgoingProgress * directionMultiplier * config.rotationAmount
150 |
151 | // Render main outgoing character if visible
152 | if (fromCharMainAlpha > 0.01f) {
153 | Text(
154 | text = fromChar.toString(),
155 | style = textStyle,
156 | color = color,
157 | textAlign = TextAlign.Center,
158 | modifier = Modifier
159 | .offset(x = fromCharMainHOffset, y = fromCharMainOffset)
160 | .alpha(fromCharMainAlpha)
161 | .blur(fromCharMainBlur, BlurredEdgeTreatment.Unbounded)
162 | .scale(fromCharMainScale)
163 | .rotate(fromCharMainRotation)
164 | )
165 | }
166 |
167 | // GHOST TRAIL EFFECT
168 | // -----------------
169 | for (i in 1..config.trailCount) {
170 | // Calculate trail position
171 | val trailPosition = i.toFloat() / config.trailCount
172 |
173 | // Apply non-linear distribution for bunching effect
174 | val trailCurve = 1f - (1f - trailPosition).pow(config.bunching)
175 |
176 | // Staggered appearance threshold
177 | val trailThreshold = trailPosition * (0.6f + letterIndex * 0.05f)
178 |
179 | if (outgoingProgress >= trailThreshold) {
180 | // Calculate ghost trail parameters
181 | val trailProgress = (outgoingProgress - trailThreshold) / (1f - trailThreshold)
182 | val trailOffsetBase = outgoingProgress * maxVerticalOffsetPx * directionMultiplier
183 | val trailOffsetVariation =
184 | sin(trailCurve * Math.PI * 0.5f) * 3f * directionMultiplier
185 | val trailOffset =
186 | (trailOffsetBase * (0.85f + trailCurve * 0.15f) + trailOffsetVariation).dp
187 |
188 | // Horizontal movement with slight variation
189 | val trailHorizontalOffset =
190 | (sin(outgoingProgress * Math.PI) * -maxHorizontalOffsetPx * horizontalSway * (1f - trailCurve * 0.3f)).dp
191 |
192 | // Opacity calculation
193 | val trailFadeMultiplier = (1f - trailCurve).pow(1.7f)
194 | val trailAlpha = fromCharMainAlpha * trailFadeMultiplier * 0.85f
195 |
196 | // Blur calculation - increases with trail position
197 | val trailBlur = (maxBlurPx * (0.6f + trailCurve * 0.6f)).dp
198 |
199 | // Scale and rotation effects
200 | val trailScale = fromCharMainScale * (1f - 0.15f * trailCurve)
201 | val trailRotation = fromCharMainRotation * (1f + trailCurve * 0.4f)
202 |
203 | // Only render visible trail instances
204 | if (trailAlpha > 0.01f) {
205 | Text(
206 | text = fromChar.toString(),
207 | style = textStyle,
208 | color = color,
209 | textAlign = TextAlign.Center,
210 | modifier = Modifier
211 | .offset(x = trailHorizontalOffset, y = trailOffset)
212 | .alpha(trailAlpha)
213 | .blur(trailBlur, BlurredEdgeTreatment.Unbounded)
214 | .scale(trailScale)
215 | .rotate(trailRotation)
216 | )
217 | }
218 | }
219 | }
220 | }
221 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/morph/DayNameMorph.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components.morph
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import androidx.compose.material3.LocalContentColor
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.text.TextStyle
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.unit.sp
18 | import java.time.LocalDate
19 | import java.util.Locale
20 | import java.time.format.TextStyle as JavaTextStyle
21 |
22 | /**
23 | * A specialized implementation of [CharacterMorph] for transitioning between day names.
24 | * This component handles extracting day names from dates and animating the character transitions.
25 | *
26 | * @param currentDate The current selected date
27 | * @param previousDate The previous date (for determining the transition)
28 | * @param isAnimating Whether the animation is currently running
29 | * @param direction The direction of the animation (FORWARD or BACKWARD)
30 | * @param config Configuration options for the animation
31 | * @param textStyle Text style to apply to the day name
32 | * @param color Text color (defaults to LocalContentColor)
33 | * @param modifier Modifier for the composable
34 | * @param onAnimationComplete Callback when animation finishes
35 | */
36 | @RequiresApi(Build.VERSION_CODES.O)
37 | @Composable
38 | fun DayNameDirectionalTransition(
39 | currentDate: LocalDate,
40 | previousDate: LocalDate?,
41 | isAnimating: Boolean,
42 | direction: TransitionDirection = TransitionDirection.FORWARD,
43 | config: DirectionalTransitionConfig = DirectionalTransitionConfig(),
44 | textStyle: TextStyle = MaterialTheme.typography.displaySmall.copy(
45 | fontSize = 32.sp,
46 | fontWeight = FontWeight.Bold
47 | ),
48 | color: Color = LocalContentColor.current,
49 | modifier: Modifier = Modifier,
50 | onAnimationComplete: () -> Unit = {}
51 | ) {
52 | // Get day names
53 | val currentDayName =
54 | currentDate.dayOfWeek.getDisplayName(JavaTextStyle.SHORT, Locale.getDefault())
55 | val previousDayName = previousDate?.dayOfWeek?.getDisplayName(
56 | JavaTextStyle.SHORT,
57 | Locale.getDefault()
58 | ) ?: currentDayName
59 |
60 | // Use the character morph animation
61 | DirectionalTextTransition(
62 | initialText = previousDayName,
63 | targetText = currentDayName,
64 | isAnimating = isAnimating,
65 | direction = direction,
66 | config = config,
67 | textStyle = textStyle,
68 | color = color,
69 | modifier = modifier,
70 | onAnimationComplete = onAnimationComplete,
71 | )
72 | }
73 |
74 | /**
75 | * A higher-level component that handles date transitions and applies the day name morphing effect.
76 | * This component automatically determines the animation direction and manages animation state.
77 | *
78 | * @param date The current date to display
79 | * @param onDateChanged Callback when date changes (useful for updating other UI elements)
80 | * @param config Configuration options for the animation
81 | * @param textStyle Text style to apply to the day name
82 | * @param color Text color
83 | * @param modifier Modifier for the composable
84 | */
85 | @RequiresApi(Build.VERSION_CODES.O)
86 | @Composable
87 | fun AnimatedDayName(
88 | date: LocalDate,
89 | onDateChanged: (LocalDate, LocalDate) -> Unit = { _, _ -> },
90 | config: DirectionalTransitionConfig = DirectionalTransitionConfig(),
91 | textStyle: TextStyle = MaterialTheme.typography.displaySmall.copy(
92 | fontSize = 32.sp,
93 | fontWeight = FontWeight.Bold
94 | ),
95 | color: Color = LocalContentColor.current,
96 | modifier: Modifier = Modifier
97 | ) {
98 | // Track previous date and animation state
99 | var previousDate by remember { mutableStateOf(null) }
100 | var isAnimating by remember { mutableStateOf(false) }
101 | var morphDirection by remember { mutableStateOf(TransitionDirection.FORWARD) }
102 |
103 | // When date changes, trigger animation
104 | LaunchedEffect(date) {
105 | if (previousDate != null && previousDate != date) {
106 | // Determine animation direction based on days of week
107 | morphDirection = determineTransitionDirection(date, previousDate!!)
108 |
109 | // Notify about date change
110 | onDateChanged(date, previousDate!!)
111 |
112 | // Start animation
113 | isAnimating = true
114 | } else if (previousDate == null) {
115 | previousDate = date
116 | }
117 | }
118 |
119 | DayNameDirectionalTransition(
120 | currentDate = date,
121 | previousDate = previousDate,
122 | isAnimating = isAnimating,
123 | direction = morphDirection,
124 | config = config,
125 | textStyle = textStyle,
126 | color = color,
127 | modifier = modifier,
128 | onAnimationComplete = {
129 | // Update previous date after animation
130 | previousDate = date
131 | isAnimating = false
132 | }
133 | )
134 | }
135 |
136 | /**
137 | * Determines the correct animation direction when transitioning between dates.
138 | * This considers the calendar flow (forward in time = bottom to top, backward = top to bottom).
139 | */
140 | @RequiresApi(Build.VERSION_CODES.O)
141 | fun determineTransitionDirection(newDate: LocalDate, oldDate: LocalDate): TransitionDirection {
142 | // Handle special case of week wrap-around
143 | if (newDate.dayOfWeek.value == 1 && oldDate.dayOfWeek.value == 7) {
144 | return TransitionDirection.FORWARD // Sunday to Monday is moving forward
145 | }
146 |
147 | if (newDate.dayOfWeek.value == 7 && oldDate.dayOfWeek.value == 1) {
148 | return TransitionDirection.BACKWARD // Monday to Sunday is moving backward
149 | }
150 |
151 | // Regular weekday progression
152 | return if (newDate.dayOfWeek.value > oldDate.dayOfWeek.value) {
153 | TransitionDirection.FORWARD
154 | } else {
155 | TransitionDirection.BACKWARD
156 | }
157 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/morph/DirectionalTextTransition.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components.morph
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.foundation.layout.wrapContentWidth
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.runtime.mutableIntStateOf
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.platform.LocalDensity
19 | import androidx.compose.ui.text.TextStyle
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.unit.dp
22 |
23 | /**
24 | * A composable that animates text transitions with a directional smearing/motion blur effect.
25 | * Characters that change will display a ghost trail effect in the specified direction.
26 | *
27 | * @param initialText The initial text state.
28 | * @param targetText The target text state to transition to.
29 | * @param isAnimating Whether the animation is currently active.
30 | * @param direction Direction of the animation.
31 | * @param config Configuration parameters for the animation effect.
32 | * @param modifier Modifier for the composable.
33 | * @param textStyle Text style for the characters.
34 | * @param color Color of the text.
35 | * @param onAnimationComplete Callback invoked when animation completes.
36 | */
37 | @Composable
38 | fun DirectionalTextTransition(
39 | initialText: String,
40 | targetText: String,
41 | isAnimating: Boolean,
42 | direction: TransitionDirection = TransitionDirection.FORWARD,
43 | config: DirectionalTransitionConfig = DirectionalTransitionConfig(),
44 | modifier: Modifier = Modifier,
45 | textStyle: TextStyle = MaterialTheme.typography.displaySmall,
46 | color: Color = Color.White,
47 | onAnimationComplete: () -> Unit = {}
48 | ) {
49 | // Determine max length to ensure stable layout
50 | val maxLength = maxOf(initialText.length, targetText.length)
51 |
52 | // Create list of character transitions
53 | val charPairs = List(maxLength) { index ->
54 | val fromChar = if (index < initialText.length) initialText[index] else ' '
55 | val toChar = if (index < targetText.length) targetText[index] else ' '
56 | Pair(fromChar, toChar)
57 | }
58 |
59 | // Track overall animation completion
60 | val animationsInProgress = remember { mutableIntStateOf(0) }
61 |
62 | LaunchedEffect(isAnimating) {
63 | if (isAnimating) {
64 | animationsInProgress.intValue = charPairs.count { it.first != it.second }
65 | }
66 | }
67 |
68 | // Overall animation completion tracking
69 | fun onCharAnimationComplete() {
70 | animationsInProgress.intValue = (animationsInProgress.intValue - 1).coerceAtLeast(0)
71 | if (animationsInProgress.intValue == 0) {
72 | onAnimationComplete()
73 | }
74 | }
75 |
76 | Row(
77 | modifier = modifier,
78 | verticalAlignment = Alignment.CenterVertically,
79 | ) {
80 | charPairs.forEachIndexed { index, (fromChar, toChar) ->
81 | Box(
82 | // modifier = Modifier.wrapContentWidth(),
83 | contentAlignment = Alignment.Center
84 | ) {
85 | if (fromChar == toChar) {
86 | // If characters are the same, just show the character without animation
87 | Text(
88 | text = toChar.toString(),
89 | style = textStyle,
90 | color = color,
91 | textAlign = TextAlign.Center
92 | )
93 | } else {
94 | // Calculate staggered delay for this character
95 | val staggerDelay = (config.staggerDelay * index * config.staggerFactor).toLong()
96 |
97 | // Animate transitioning characters
98 | CharacterTransition(
99 | fromChar = fromChar,
100 | toChar = toChar,
101 | isAnimating = isAnimating,
102 | direction = direction,
103 | config = config,
104 | delayMillis = staggerDelay,
105 | textStyle = textStyle,
106 | color = color,
107 | letterIndex = index,
108 | onComplete = { onCharAnimationComplete() }
109 | )
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/morph/DirectionalTransitionConfig.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components.morph
2 |
3 | import androidx.compose.animation.core.*
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.alpha
11 | import androidx.compose.ui.draw.blur
12 | import androidx.compose.ui.draw.rotate
13 | import androidx.compose.ui.draw.scale
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.platform.LocalDensity
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 | import kotlinx.coroutines.delay
21 | import kotlinx.coroutines.launch
22 | import kotlin.math.pow
23 | import kotlin.math.sin
24 |
25 | /**
26 | * Configuration for the DirectionalTextTransition effect.
27 | *
28 | * @param transitionDuration Duration of the transition animation in milliseconds.
29 | * @param staggerDelay Base delay between character animations in milliseconds.
30 | * @param staggerFactor Multiplier for stagger delay between subsequent characters.
31 | * @param trailCount Number of ghost trail instances for each character.
32 | * @param maxVerticalOffset Maximum vertical movement distance during animation.
33 | * @param maxHorizontalOffset Maximum horizontal sway during animation.
34 | * @param maxBlur Maximum blur amount applied to trailing instances.
35 | * @param rotationAmount Maximum rotation angle in degrees.
36 | * @param scaleRange Pair of (minimum, maximum) scale values during transition.
37 | * @param charSpacing Horizontal spacing between characters.
38 | * @param bunching Power factor for trail bunching (higher = more bunched).
39 | * @param easingCurve Easing curve for the animation.
40 | */
41 | data class DirectionalTransitionConfig(
42 | val transitionDuration: Int = 750,
43 | val staggerDelay: Long = 120,
44 | val staggerFactor: Float = 1.3f,
45 | val trailCount: Int = 8,
46 | val maxVerticalOffset: Dp = 48.dp,
47 | val maxHorizontalOffset: Dp = 3.dp,
48 | val maxBlur: Dp = 10.dp,
49 | val rotationAmount: Float = 6f,
50 | val scaleRange: Pair = Pair(0.6f, 1.15f),
51 | val charSpacing: Dp = 2.dp,
52 | val bunching: Float = 2.7f,
53 | val easingCurve: Easing = FastOutSlowInEasing
54 | )
55 |
56 | /**
57 | * Direction of the transition animation.
58 | */
59 | enum class TransitionDirection {
60 | FORWARD, BACKWARD
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/calendar/components/morph/TransitionControlPanel.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.calendar.components.morph
2 |
3 |
4 | import android.os.Build
5 | import androidx.annotation.RequiresApi
6 | import androidx.compose.animation.AnimatedVisibility
7 | import androidx.compose.animation.core.animateFloatAsState
8 | import androidx.compose.animation.expandVertically
9 | import androidx.compose.animation.fadeIn
10 | import androidx.compose.animation.fadeOut
11 | import androidx.compose.animation.shrinkVertically
12 | import androidx.compose.foundation.BorderStroke
13 | import androidx.compose.foundation.background
14 | import androidx.compose.foundation.clickable
15 | import androidx.compose.foundation.layout.Arrangement
16 | import androidx.compose.foundation.layout.Box
17 | import androidx.compose.foundation.layout.Column
18 | import androidx.compose.foundation.layout.Row
19 | import androidx.compose.foundation.layout.Spacer
20 | import androidx.compose.foundation.layout.fillMaxWidth
21 | import androidx.compose.foundation.layout.height
22 | import androidx.compose.foundation.layout.padding
23 | import androidx.compose.foundation.layout.size
24 | import androidx.compose.foundation.layout.width
25 | import androidx.compose.foundation.rememberScrollState
26 | import androidx.compose.foundation.shape.CircleShape
27 | import androidx.compose.foundation.shape.RoundedCornerShape
28 | import androidx.compose.foundation.verticalScroll
29 | import androidx.compose.material.icons.Icons
30 | import androidx.compose.material.icons.filled.Add
31 | import androidx.compose.material.icons.filled.Close
32 | import androidx.compose.material.icons.filled.Info
33 | import androidx.compose.material.icons.filled.KeyboardArrowDown
34 | import androidx.compose.material.icons.filled.KeyboardArrowUp
35 | import androidx.compose.material.icons.rounded.Check
36 | import androidx.compose.material.icons.rounded.Edit
37 | import androidx.compose.material.icons.rounded.Refresh
38 | import androidx.compose.material.icons.rounded.Star
39 | import androidx.compose.material3.Button
40 | import androidx.compose.material3.ButtonDefaults
41 | import androidx.compose.material3.Card
42 | import androidx.compose.material3.CardDefaults
43 | import androidx.compose.material3.Divider
44 | import androidx.compose.material3.FilledIconButton
45 | import androidx.compose.material3.FilledTonalButton
46 | import androidx.compose.material3.FilledTonalIconButton
47 | import androidx.compose.material3.Icon
48 | import androidx.compose.material3.IconButton
49 | import androidx.compose.material3.MaterialTheme
50 | import androidx.compose.material3.OutlinedCard
51 | import androidx.compose.material3.Slider
52 | import androidx.compose.material3.SliderDefaults
53 | import androidx.compose.material3.Surface
54 | import androidx.compose.material3.Switch
55 | import androidx.compose.material3.Text
56 | import androidx.compose.material3.TextButton
57 | import androidx.compose.material3.TooltipDefaults
58 | import androidx.compose.material3.surfaceColorAtElevation
59 | import androidx.compose.runtime.Composable
60 | import androidx.compose.runtime.LaunchedEffect
61 | import androidx.compose.runtime.getValue
62 | import androidx.compose.runtime.mutableFloatStateOf
63 | import androidx.compose.runtime.mutableIntStateOf
64 | import androidx.compose.runtime.mutableStateOf
65 | import androidx.compose.runtime.remember
66 | import androidx.compose.runtime.rememberCoroutineScope
67 | import androidx.compose.runtime.setValue
68 | import androidx.compose.ui.Alignment
69 | import androidx.compose.ui.Modifier
70 | import androidx.compose.ui.draw.alpha
71 | import androidx.compose.ui.draw.clip
72 | import androidx.compose.ui.graphics.Color
73 | import androidx.compose.ui.graphics.vector.ImageVector
74 | import androidx.compose.ui.text.font.FontWeight
75 | import androidx.compose.ui.text.style.TextAlign
76 | import androidx.compose.ui.unit.dp
77 | import androidx.compose.ui.unit.sp
78 | import com.binissa.calendar.calendar.components.morph.AnimatedDayName
79 | import com.binissa.calendar.calendar.components.morph.DirectionalTransitionConfig
80 | import com.binissa.calendar.calendar.components.morph.TransitionDirection
81 | import kotlinx.coroutines.delay
82 | import kotlinx.coroutines.launch
83 | import java.time.DayOfWeek
84 | import java.time.LocalDate
85 | import java.time.format.DateTimeFormatter
86 | import java.time.format.TextStyle
87 | import java.util.Locale
88 |
89 | @RequiresApi(Build.VERSION_CODES.O)
90 | @Composable
91 | fun TransitionControlPanel(
92 | modifier: Modifier = Modifier,
93 | initialConfig: DirectionalTransitionConfig = DirectionalTransitionConfig(),
94 | onConfigChanged: (DirectionalTransitionConfig) -> Unit
95 | ) {
96 | var config by remember { mutableStateOf(initialConfig) }
97 | var currentDate by remember { mutableStateOf(LocalDate.now()) }
98 | var expandedSection by remember { mutableStateOf("none") }
99 | val scope = rememberCoroutineScope()
100 |
101 | // Define presets with visual indicators
102 | val presets = remember {
103 | listOf(
104 | TransitionPreset(
105 | name = "Subtle",
106 | description = "Gentle motion with minimal blur",
107 | visualTags = listOf("Clean", "Simple"),
108 | config = DirectionalTransitionConfig(
109 | transitionDuration = 400,
110 | staggerDelay = 70,
111 | trailCount = 2,
112 | maxBlur = 4.dp,
113 | rotationAmount = 0f,
114 | maxVerticalOffset = 12.dp,
115 | maxHorizontalOffset = 1.dp,
116 | bunching = 1.2f
117 | )
118 | ),
119 | TransitionPreset(
120 | name = "Classic Fade",
121 | description = "Traditional text crossfade with motion",
122 | visualTags = listOf("Smooth", "Professional"),
123 | config = DirectionalTransitionConfig(
124 | transitionDuration = 500,
125 | staggerDelay = 80,
126 | trailCount = 3,
127 | maxBlur = 8.dp,
128 | rotationAmount = 0f,
129 | maxVerticalOffset = 20.dp,
130 | maxHorizontalOffset = 0.dp,
131 | bunching = 1.5f
132 | )
133 | ),
134 | TransitionPreset(
135 | name = "Smear",
136 | description = "Dramatic motion blur effect",
137 | visualTags = listOf("Dynamic", "Bold"),
138 | config = DirectionalTransitionConfig(
139 | transitionDuration = 650,
140 | staggerDelay = 60,
141 | trailCount = 7,
142 | maxBlur = 16.dp,
143 | rotationAmount = 3f,
144 | maxVerticalOffset = 30.dp,
145 | maxHorizontalOffset = 2.dp,
146 | bunching = 2.2f
147 | )
148 | ),
149 | TransitionPreset(
150 | name = "Ghostly",
151 | description = "Extended blur trails with transparency",
152 | visualTags = listOf("Ethereal", "Artistic"),
153 | config = DirectionalTransitionConfig(
154 | transitionDuration = 800,
155 | staggerDelay = 90,
156 | trailCount = 9,
157 | maxBlur = 20.dp,
158 | rotationAmount = 0f,
159 | maxVerticalOffset = 35.dp,
160 | maxHorizontalOffset = 0.dp,
161 | bunching = 3.0f
162 | )
163 | ),
164 | TransitionPreset(
165 | name = "Playful",
166 | description = "Bouncy motion with rotation",
167 | visualTags = listOf("Fun", "Energetic"),
168 | config = DirectionalTransitionConfig(
169 | transitionDuration = 700,
170 | staggerDelay = 100,
171 | trailCount = 5,
172 | maxBlur = 10.dp,
173 | rotationAmount = 10f,
174 | maxVerticalOffset = 25.dp,
175 | maxHorizontalOffset = 5.dp,
176 | bunching = 1.8f,
177 | scaleRange = Pair(0.7f, 1.2f)
178 | )
179 | ),
180 | TransitionPreset(
181 | name = "Techno",
182 | description = "Sharp, digital appearance",
183 | visualTags = listOf("Modern", "Precise"),
184 | config = DirectionalTransitionConfig(
185 | transitionDuration = 550,
186 | staggerDelay = 50,
187 | trailCount = 4,
188 | maxBlur = 6.dp,
189 | rotationAmount = 2f,
190 | maxVerticalOffset = 28.dp,
191 | maxHorizontalOffset = 1.dp,
192 | bunching = 0.8f,
193 | scaleRange = Pair(0.9f, 1.1f)
194 | )
195 | )
196 | )
197 | }
198 |
199 | var selectedPreset by remember { mutableStateOf(presets.first()) }
200 | var isCustom by remember { mutableStateOf(false) }
201 |
202 | LaunchedEffect(config) {
203 | onConfigChanged(config)
204 |
205 | // Check if config matches a preset
206 | val matchingPreset = presets.firstOrNull { it.config == config }
207 | if (matchingPreset != null) {
208 | selectedPreset = matchingPreset
209 | isCustom = false
210 | } else {
211 | isCustom = true
212 | }
213 | }
214 |
215 | // Animate the date change for preview
216 | var isAnimating by remember { mutableStateOf(false) }
217 | fun animatePreview(newDate: LocalDate) {
218 | scope.launch {
219 | isAnimating = true
220 | currentDate = newDate
221 | delay(config.transitionDuration.toLong() + 200)
222 | isAnimating = false
223 | }
224 | }
225 |
226 | Column(
227 | modifier = modifier
228 | .padding(16.dp)
229 | .verticalScroll(rememberScrollState()),
230 | verticalArrangement = Arrangement.spacedBy(16.dp)
231 | ) {
232 | // Preview section with elegant card
233 | Card(
234 | modifier = Modifier.fillMaxWidth(),
235 | elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
236 | colors = CardDefaults.cardColors(
237 | containerColor = MaterialTheme.colorScheme.surfaceVariant,
238 | ),
239 | shape = RoundedCornerShape(16.dp)
240 | ) {
241 | Column(
242 | modifier = Modifier.padding(16.dp),
243 | horizontalAlignment = Alignment.CenterHorizontally,
244 | verticalArrangement = Arrangement.spacedBy(16.dp)
245 | ) {
246 | Text(
247 | text = "Preview",
248 | style = MaterialTheme.typography.titleMedium,
249 | color = MaterialTheme.colorScheme.onSurfaceVariant
250 | )
251 |
252 | // Animation preview
253 | Box(
254 | modifier = Modifier
255 | .fillMaxWidth()
256 | .padding(vertical = 24.dp),
257 | contentAlignment = Alignment.Center
258 | ) {
259 | AnimatedDayName(
260 | date = currentDate,
261 | config = config,
262 | textStyle = MaterialTheme.typography.displayMedium.copy(
263 | fontWeight = FontWeight.Bold
264 | ),
265 | color = MaterialTheme.colorScheme.primary
266 | )
267 | }
268 |
269 | // Day selection buttons in an elegant row
270 | Row(
271 | modifier = Modifier.fillMaxWidth(),
272 | horizontalArrangement = Arrangement.SpaceEvenly
273 | ) {
274 | // Previous/Next buttons
275 | FilledTonalIconButton(
276 | onClick = { animatePreview(currentDate.minusDays(1)) },
277 | enabled = !isAnimating
278 | ) {
279 | Icon(
280 | imageVector = Icons.Default.Close,
281 | contentDescription = "Previous Day"
282 | )
283 | }
284 |
285 | // Days of week buttons
286 | DayOfWeek.values().take(3).forEach { day ->
287 | DayButton(
288 | day = day,
289 | isSelected = currentDate.dayOfWeek == day,
290 | onClick = { animatePreview(LocalDate.now().with(day)) },
291 | enabled = !isAnimating
292 | )
293 | }
294 |
295 | FilledTonalIconButton(
296 | onClick = { animatePreview(currentDate.plusDays(1)) },
297 | enabled = !isAnimating
298 | ) {
299 | Icon(
300 | imageVector = Icons.Default.Add,
301 | contentDescription = "Next Day"
302 | )
303 | }
304 | }
305 |
306 | Row(
307 | modifier = Modifier.fillMaxWidth(),
308 | horizontalArrangement = Arrangement.Center
309 | ) {
310 | DayOfWeek.values().drop(3).forEach { day ->
311 | DayButton(
312 | day = day,
313 | isSelected = currentDate.dayOfWeek == day,
314 | onClick = { animatePreview(LocalDate.now().with(day)) },
315 | enabled = !isAnimating
316 | )
317 | }
318 | }
319 | }
320 | }
321 |
322 | // Presets selector with visual indicators
323 | SectionHeader(
324 | title = "Effect Style",
325 | icon = Icons.Rounded.Star,
326 | expanded = expandedSection == "presets",
327 | onExpandToggle = { expandedSection = if (expandedSection == "presets") "none" else "presets" }
328 | )
329 |
330 | AnimatedVisibility(
331 | visible = expandedSection == "presets",
332 | enter = fadeIn() + expandVertically(),
333 | exit = fadeOut() + shrinkVertically()
334 | ) {
335 | PresetSelector(
336 | presets = presets,
337 | selectedPreset = if (isCustom) null else selectedPreset,
338 | onPresetSelected = { preset ->
339 | config = preset.config
340 | selectedPreset = preset
341 | isCustom = false
342 | }
343 | )
344 | }
345 |
346 | // Quick controls for most common adjustments
347 | SectionHeader(
348 | title = "Quick Adjustments",
349 | icon = Icons.Rounded.Edit,
350 | expanded = expandedSection == "quick",
351 | onExpandToggle = { expandedSection = if (expandedSection == "quick") "none" else "quick" }
352 | )
353 |
354 | AnimatedVisibility(
355 | visible = expandedSection == "quick",
356 | enter = fadeIn() + expandVertically(),
357 | exit = fadeOut() + shrinkVertically()
358 | ) {
359 | QuickAdjustments(
360 | config = config,
361 | onConfigChanged = { newConfig ->
362 | config = newConfig
363 | isCustom = true
364 | }
365 | )
366 | }
367 |
368 | // Advanced controls
369 | SectionHeader(
370 | title = "Advanced Settings",
371 | icon = Icons.Rounded.Edit,
372 | expanded = expandedSection == "advanced",
373 | onExpandToggle = { expandedSection = if (expandedSection == "advanced") "none" else "advanced" }
374 | )
375 |
376 | AnimatedVisibility(
377 | visible = expandedSection == "advanced",
378 | enter = fadeIn() + expandVertically(),
379 | exit = fadeOut() + shrinkVertically()
380 | ) {
381 | AdvancedControls(
382 | config = config,
383 | onConfigChanged = { newConfig ->
384 | config = newConfig
385 | isCustom = true
386 | }
387 | )
388 | }
389 |
390 | // Reset/Save button row
391 | Row(
392 | modifier = Modifier
393 | .fillMaxWidth()
394 | .padding(vertical = 8.dp),
395 | horizontalArrangement = Arrangement.SpaceBetween
396 | ) {
397 | TextButton(
398 | onClick = {
399 | config = DirectionalTransitionConfig()
400 | isCustom = false
401 | }
402 | ) {
403 | Icon(
404 | imageVector = Icons.Rounded.Refresh,
405 | contentDescription = "Reset",
406 | modifier = Modifier.size(18.dp)
407 | )
408 | Spacer(modifier = Modifier.width(4.dp))
409 | Text("Reset")
410 | }
411 |
412 | if (isCustom) {
413 | Button(
414 | onClick = {
415 | // Save as custom preset (in a real app)
416 | }
417 | ) {
418 | Text("Save Custom Preset")
419 | }
420 | }
421 | }
422 | }
423 | }
424 |
425 | @Composable
426 | private fun SectionHeader(
427 | title: String,
428 | icon: ImageVector,
429 | expanded: Boolean,
430 | onExpandToggle: () -> Unit
431 | ) {
432 | Row(
433 | modifier = Modifier
434 | .fillMaxWidth()
435 | .clickable(onClick = onExpandToggle)
436 | .padding(vertical = 8.dp),
437 | verticalAlignment = Alignment.CenterVertically,
438 | horizontalArrangement = Arrangement.SpaceBetween
439 | ) {
440 | Row(
441 | verticalAlignment = Alignment.CenterVertically
442 | ) {
443 | Icon(
444 | imageVector = icon,
445 | contentDescription = null,
446 | tint = MaterialTheme.colorScheme.primary,
447 | modifier = Modifier.size(24.dp)
448 | )
449 | Spacer(modifier = Modifier.width(8.dp))
450 | Text(
451 | text = title,
452 | style = MaterialTheme.typography.titleMedium,
453 | fontWeight = FontWeight.Bold,
454 | color = MaterialTheme.colorScheme.onSurface
455 | )
456 | }
457 |
458 | Icon(
459 | imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
460 | contentDescription = if (expanded) "Collapse" else "Expand",
461 | tint = MaterialTheme.colorScheme.onSurfaceVariant
462 | )
463 | }
464 | Divider(color = MaterialTheme.colorScheme.outlineVariant)
465 | }
466 |
467 | @Composable
468 | private fun PresetSelector(
469 | presets: List,
470 | selectedPreset: TransitionPreset?,
471 | onPresetSelected: (TransitionPreset) -> Unit
472 | ) {
473 | Column(
474 | verticalArrangement = Arrangement.spacedBy(8.dp),
475 | modifier = Modifier.padding(vertical = 8.dp)
476 | ) {
477 | presets.chunked(2).forEach { rowPresets ->
478 | Row(
479 | horizontalArrangement = Arrangement.spacedBy(8.dp),
480 | modifier = Modifier.fillMaxWidth()
481 | ) {
482 | rowPresets.forEach { preset ->
483 | val isSelected = preset == selectedPreset
484 |
485 | PresetCard(
486 | preset = preset,
487 | isSelected = isSelected,
488 | onClick = { onPresetSelected(preset) },
489 | modifier = Modifier.weight(1f)
490 | )
491 | }
492 |
493 | // Add empty space if odd number
494 | if (rowPresets.size == 1) {
495 | Spacer(modifier = Modifier.weight(1f))
496 | }
497 | }
498 | }
499 | }
500 | }
501 |
502 | @Composable
503 | private fun PresetCard(
504 | preset: TransitionPreset,
505 | isSelected: Boolean,
506 | onClick: () -> Unit,
507 | modifier: Modifier = Modifier
508 | ) {
509 | val borderColor = if (isSelected) {
510 | MaterialTheme.colorScheme.primary
511 | } else {
512 | MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
513 | }
514 |
515 | val backgroundColor = if (isSelected) {
516 | MaterialTheme.colorScheme.primaryContainer
517 | } else {
518 | MaterialTheme.colorScheme.surfaceVariant
519 | }
520 |
521 | val textColor = if (isSelected) {
522 | MaterialTheme.colorScheme.onPrimaryContainer
523 | } else {
524 | MaterialTheme.colorScheme.onSurfaceVariant
525 | }
526 |
527 | OutlinedCard(
528 | onClick = onClick,
529 | modifier = modifier,
530 | colors = CardDefaults.outlinedCardColors(
531 | containerColor = backgroundColor,
532 | contentColor = textColor
533 | ),
534 | border = BorderStroke(2.dp, borderColor)
535 | ) {
536 | Column(
537 | modifier = Modifier.padding(12.dp),
538 | verticalArrangement = Arrangement.spacedBy(4.dp)
539 | ) {
540 | Row(
541 | verticalAlignment = Alignment.CenterVertically,
542 | horizontalArrangement = Arrangement.SpaceBetween,
543 | modifier = Modifier.fillMaxWidth()
544 | ) {
545 | Text(
546 | text = preset.name,
547 | style = MaterialTheme.typography.titleMedium,
548 | fontWeight = FontWeight.Bold
549 | )
550 |
551 | if (isSelected) {
552 | Icon(
553 | imageVector = Icons.Rounded.Check,
554 | contentDescription = "Selected",
555 | tint = MaterialTheme.colorScheme.primary,
556 | modifier = Modifier.size(18.dp)
557 | )
558 | }
559 | }
560 |
561 | Text(
562 | text = preset.description,
563 | style = MaterialTheme.typography.bodySmall,
564 | color = if (isSelected) {
565 | MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
566 | } else {
567 | MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
568 | }
569 | )
570 |
571 | Spacer(modifier = Modifier.height(4.dp))
572 |
573 | // Visual tags
574 | Row(
575 | horizontalArrangement = Arrangement.spacedBy(4.dp)
576 | ) {
577 | preset.visualTags.forEach { tag ->
578 | PresetTag(tag = tag, isSelected = isSelected)
579 | }
580 | }
581 | }
582 | }
583 | }
584 |
585 | @Composable
586 | private fun PresetTag(tag: String, isSelected: Boolean) {
587 | val backgroundColor = if (isSelected) {
588 | MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
589 | } else {
590 | MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f)
591 | }
592 |
593 | val textColor = if (isSelected) {
594 | MaterialTheme.colorScheme.primary
595 | } else {
596 | MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
597 | }
598 |
599 | Box(
600 | modifier = Modifier
601 | .clip(RoundedCornerShape(4.dp))
602 | .background(backgroundColor)
603 | .padding(horizontal = 6.dp, vertical = 2.dp)
604 | ) {
605 | Text(
606 | text = tag,
607 | style = MaterialTheme.typography.labelSmall,
608 | color = textColor
609 | )
610 | }
611 | }
612 |
613 | @Composable
614 | private fun QuickAdjustments(
615 | config: DirectionalTransitionConfig,
616 | onConfigChanged: (DirectionalTransitionConfig) -> Unit
617 | ) {
618 | Column(
619 | verticalArrangement = Arrangement.spacedBy(16.dp),
620 | modifier = Modifier.padding(vertical = 8.dp)
621 | ) {
622 | // Intensity slider
623 | var intensity by remember { mutableFloatStateOf(calculateIntensity(config)) }
624 |
625 | LabeledSlider(
626 | label = "Effect Intensity",
627 | value = intensity,
628 | range = 0f..1f,
629 | steps = 10,
630 | valueDisplay = { "%.0f%%".format(it * 100) },
631 | onValueChange = {
632 | intensity = it
633 | onConfigChanged(applyIntensity(config, intensity))
634 | }
635 | )
636 |
637 | // Blur amount
638 | LabeledSlider(
639 | label = "Blur Amount",
640 | value = config.maxBlur.value,
641 | range = 0f..20f,
642 | valueDisplay = { "%.1f".format(it) },
643 | onValueChange = { onConfigChanged(config.copy(maxBlur = it.dp)) }
644 | )
645 |
646 | // Trail count with tooltip
647 | SliderWithHelp(
648 | label = "Trail Count",
649 | value = config.trailCount.toFloat(),
650 | range = 1f..12f,
651 | steps = 11,
652 | valueDisplay = { "${it.toInt()}" },
653 | helpText = "Number of trailing ghost instances. Higher values create more pronounced motion blur effect.",
654 | onValueChange = { onConfigChanged(config.copy(trailCount = it.toInt())) }
655 | )
656 |
657 | // Duration slider
658 | LabeledSlider(
659 | label = "Duration (ms)",
660 | value = config.transitionDuration.toFloat(),
661 | range = 200f..1000f,
662 | steps = 16,
663 | valueDisplay = { "${it.toInt()}" },
664 | onValueChange = { onConfigChanged(config.copy(transitionDuration = it.toInt())) }
665 | )
666 | }
667 | }
668 |
669 | @Composable
670 | private fun AdvancedControls(
671 | config: DirectionalTransitionConfig,
672 | onConfigChanged: (DirectionalTransitionConfig) -> Unit
673 | ) {
674 | Column(
675 | verticalArrangement = Arrangement.spacedBy(16.dp),
676 | modifier = Modifier.padding(vertical = 8.dp)
677 | ) {
678 | // Group sliders into categories
679 | SectionSubheader("Animation Timing")
680 |
681 | LabeledSlider(
682 | label = "Stagger Delay (ms)",
683 | value = config.staggerDelay.toFloat(),
684 | range = 0f..200f,
685 | steps = 20,
686 | valueDisplay = { "${it.toInt()}" },
687 | onValueChange = { onConfigChanged(config.copy(staggerDelay = it.toLong())) }
688 | )
689 |
690 | LabeledSlider(
691 | label = "Stagger Factor",
692 | value = config.staggerFactor,
693 | range = 0.5f..3f,
694 | valueDisplay = { "%.1f".format(it) },
695 | onValueChange = { onConfigChanged(config.copy(staggerFactor = it)) }
696 | )
697 |
698 | SectionSubheader("Visual Effects")
699 |
700 | SliderWithHelp(
701 | label = "Bunching Factor",
702 | value = config.bunching,
703 | range = 0.5f..5f,
704 | valueDisplay = { "%.1f".format(it) },
705 | helpText = "Controls how trails bunch together. Higher values create a more concentrated smearing effect at the end of the trail.",
706 | onValueChange = { onConfigChanged(config.copy(bunching = it)) }
707 | )
708 |
709 | LabeledSlider(
710 | label = "Rotation (°)",
711 | value = config.rotationAmount,
712 | range = 0f..20f,
713 | valueDisplay = { "%.1f°".format(it) },
714 | onValueChange = { onConfigChanged(config.copy(rotationAmount = it)) }
715 | )
716 |
717 | SectionSubheader("Movement")
718 |
719 | LabeledSlider(
720 | label = "Vertical Offset",
721 | value = config.maxVerticalOffset.value,
722 | range = 0f..60f,
723 | valueDisplay = { "%.0f".format(it) },
724 | onValueChange = { onConfigChanged(config.copy(maxVerticalOffset = it.dp)) }
725 | )
726 |
727 | LabeledSlider(
728 | label = "Horizontal Offset",
729 | value = config.maxHorizontalOffset.value,
730 | range = 0f..10f,
731 | valueDisplay = { "%.1f".format(it) },
732 | onValueChange = { onConfigChanged(config.copy(maxHorizontalOffset = it.dp)) }
733 | )
734 |
735 | SectionSubheader("Scale Effects")
736 |
737 | LabeledSlider(
738 | label = "Min Scale",
739 | value = config.scaleRange.first,
740 | range = 0.5f..1f,
741 | valueDisplay = { "%.2f".format(it) },
742 | onValueChange = {
743 | onConfigChanged(config.copy(
744 | scaleRange = Pair(it, config.scaleRange.second)
745 | ))
746 | }
747 | )
748 |
749 | LabeledSlider(
750 | label = "Max Scale",
751 | value = config.scaleRange.second,
752 | range = 1f..1.5f,
753 | valueDisplay = { "%.2f".format(it) },
754 | onValueChange = {
755 | onConfigChanged(config.copy(
756 | scaleRange = Pair(config.scaleRange.first, it)
757 | ))
758 | }
759 | )
760 | }
761 | }
762 |
763 | @Composable
764 | private fun SectionSubheader(text: String) {
765 | Text(
766 | text = text,
767 | style = MaterialTheme.typography.titleSmall,
768 | color = MaterialTheme.colorScheme.primary,
769 | modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
770 | )
771 | }
772 |
773 | @Composable
774 | private fun LabeledSlider(
775 | label: String,
776 | value: Float,
777 | range: ClosedFloatingPointRange,
778 | steps: Int = 0,
779 | valueDisplay: (Float) -> String = { "%.1f".format(it) },
780 | onValueChange: (Float) -> Unit
781 | ) {
782 | Column {
783 | Row(
784 | horizontalArrangement = Arrangement.SpaceBetween,
785 | modifier = Modifier.fillMaxWidth(),
786 | verticalAlignment = Alignment.CenterVertically
787 | ) {
788 | Text(
789 | text = label,
790 | style = MaterialTheme.typography.bodyMedium,
791 | color = MaterialTheme.colorScheme.onSurface
792 | )
793 |
794 | Text(
795 | text = valueDisplay(value),
796 | style = MaterialTheme.typography.bodySmall,
797 | color = MaterialTheme.colorScheme.onSurfaceVariant
798 | )
799 | }
800 |
801 | Slider(
802 | value = value,
803 | onValueChange = onValueChange,
804 | valueRange = range,
805 | steps = steps,
806 | modifier = Modifier.fillMaxWidth(),
807 | colors = SliderDefaults.colors(
808 | thumbColor = MaterialTheme.colorScheme.primary,
809 | activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
810 | )
811 | )
812 | }
813 | }
814 |
815 | @Composable
816 | private fun SliderWithHelp(
817 | label: String,
818 | value: Float,
819 | range: ClosedFloatingPointRange,
820 | steps: Int = 0,
821 | valueDisplay: (Float) -> String = { "%.1f".format(it) },
822 | helpText: String,
823 | onValueChange: (Float) -> Unit
824 | ) {
825 | Column {
826 | Row(
827 | horizontalArrangement = Arrangement.SpaceBetween,
828 | modifier = Modifier.fillMaxWidth(),
829 | verticalAlignment = Alignment.CenterVertically
830 | ) {
831 | Row(
832 | verticalAlignment = Alignment.CenterVertically
833 | ) {
834 | Text(
835 | text = label,
836 | style = MaterialTheme.typography.bodyMedium,
837 | color = MaterialTheme.colorScheme.onSurface
838 | )
839 | }
840 |
841 | Text(
842 | text = valueDisplay(value),
843 | style = MaterialTheme.typography.bodySmall,
844 | color = MaterialTheme.colorScheme.onSurfaceVariant
845 | )
846 | }
847 |
848 | Slider(
849 | value = value,
850 | onValueChange = onValueChange,
851 | valueRange = range,
852 | steps = steps,
853 | modifier = Modifier.fillMaxWidth(),
854 | colors = SliderDefaults.colors(
855 | thumbColor = MaterialTheme.colorScheme.primary,
856 | activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
857 | )
858 | )
859 | }
860 | }
861 |
862 | @RequiresApi(Build.VERSION_CODES.O)
863 | @Composable
864 | private fun DayButton(
865 | day: DayOfWeek,
866 | isSelected: Boolean,
867 | onClick: () -> Unit,
868 | enabled: Boolean = true
869 | ) {
870 | val dayName = day.getDisplayName(TextStyle.SHORT, Locale.getDefault())
871 | val buttonColors = if (isSelected) {
872 | ButtonDefaults.filledTonalButtonColors(
873 | containerColor = MaterialTheme.colorScheme.primaryContainer,
874 | contentColor = MaterialTheme.colorScheme.onPrimaryContainer
875 | )
876 | } else {
877 | ButtonDefaults.filledTonalButtonColors()
878 | }
879 |
880 | FilledTonalButton(
881 | onClick = onClick,
882 | modifier = Modifier.padding(horizontal = 4.dp),
883 | enabled = enabled,
884 | colors = buttonColors
885 | ) {
886 | Text(
887 | text = dayName.take(1),
888 | fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
889 | )
890 | }
891 | }
892 |
893 | // Helper functions to calculate and apply intensity
894 | private fun calculateIntensity(config: DirectionalTransitionConfig): Float {
895 | // Simplified - in reality would need more advanced normalization
896 | val blurFactor = config.maxBlur.value / 20f
897 | val trailFactor = config.trailCount / 12f
898 | val verticalFactor = config.maxVerticalOffset.value / 60f
899 |
900 | return (blurFactor + trailFactor + verticalFactor) / 3f
901 | }
902 |
903 | private fun applyIntensity(config: DirectionalTransitionConfig, intensity: Float): DirectionalTransitionConfig {
904 | // Scale key parameters based on intensity
905 | return config.copy(
906 | maxBlur = (intensity * 20f).dp,
907 | trailCount = (1 + intensity * 11).toInt(),
908 | maxVerticalOffset = (intensity * 60f).dp,
909 | bunching = 1f + intensity * 4f
910 | )
911 | }
912 |
913 | // Data class for presets
914 | data class TransitionPreset(
915 | val name: String,
916 | val description: String,
917 | val visualTags: List,
918 | val config: DirectionalTransitionConfig
919 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun CalendarTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/binissa/calendar/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Calendar
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/binissa/calendar/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.calendar
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | alias(libs.plugins.ksp) apply false
7 | alias(libs.plugins.hilt) apply false
8 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.8.2"
3 | hiltNavigationCompose = "1.2.0"
4 | kotlin = "2.0.0"
5 | kotlinKsp = "2.0.0-1.0.23"
6 | coreKtx = "1.15.0"
7 | junit = "4.13.2"
8 | junitVersion = "1.2.1"
9 | espressoCore = "3.6.1"
10 | lifecycleRuntimeKtx = "2.8.7"
11 | activityCompose = "1.10.1"
12 | composeBom = "2025.03.00"
13 | hiltAndroid = "2.55"
14 |
15 | [libraries]
16 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
17 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
18 | junit = { group = "junit", name = "junit", version.ref = "junit" }
19 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
20 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
21 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
22 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
23 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
24 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
25 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
26 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
27 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
28 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
29 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
30 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
31 |
32 | dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "hiltAndroid" }
33 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
34 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroid" }
35 |
36 |
37 | [plugins]
38 | android-application = { id = "com.android.application", version.ref = "agp" }
39 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
40 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
41 | ksp = { id = "com.google.devtools.ksp", version.ref = "kotlinKsp" }
42 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
43 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/MorphTextTransition/4ffaaca4a1c9b598addbbd22bebe6a2232e2c891/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 16 15:38:10 EET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Calendar"
23 | include(":app")
24 |
--------------------------------------------------------------------------------