From aede45cec202f8cb4b4b2cdf57fe8a9b13590c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Sat, 31 Jan 2026 12:51:37 +0100 Subject: [PATCH] Bump version to 1.3 (versionCode 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CityListSheet for managing locations via page indicator tap - Add drag-to-reorder support in city list - Fix Polish translation: "Daylight w roku" → "Długość dnia w roku" - Fix PageIndicator position to avoid overlapping yearly chart - Remove "About" section from UI (simplified) - Add CHANGELOG.md --- CHANGELOG.md | 42 +++ README.md | 2 +- app/build.gradle.kts | 4 +- .../java/com/sunzones/ui/main/MainScreen.kt | 17 +- .../ui/main/components/CityListSheet.kt | 269 ++++++++++++++++++ .../ui/main/components/PageIndicator.kt | 9 +- .../sunzones/ui/main/components/SunCard.kt | 69 ----- app/src/main/res/values-pl/strings.xml | 4 +- app/src/main/res/values/strings.xml | 2 + 9 files changed, 336 insertions(+), 82 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 app/src/main/java/com/sunzones/ui/main/components/CityListSheet.kt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6ba8e1e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +## [1.3] - 2026-01-31 + +### Added +- City management sheet - tap page indicator to manage, reorder and delete locations +- Drag-to-reorder support in city list + +### Changed +- Page indicator moved higher to avoid overlapping yearly daylight chart +- Simplified page indicator styling + +### Fixed +- Polish translation: "Daylight w roku" → "Długość dnia w roku" + +### Removed +- "About" section from cards and city list (simplified UI) + +## [1.2] - 2025-01-XX + +### Added +- Location reordering support +- Improved page transitions between locations + +## [1.1] - 2025-01-XX + +### Added +- Sun data refresh on app resume +- Periodic 60-second auto-refresh + +### Fixed +- Location name text alignment for multi-line names + +## [1.0] - 2025-01-XX + +### Added +- Initial release +- Multi-location sunrise/sunset tracking +- Animated sun/moon arc +- Time-of-day gradient backgrounds +- Yearly daylight chart +- 20 language translations diff --git a/README.md b/README.md index f3934e6..21f2320 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Track sunrise, sunset and daylight across multiple locations around the world. A ## Features -- **Multi-location tracking** -- swipe between saved locations to see real-time sun and moon data +- **Multi-location tracking** -- swipe between saved locations to see real-time sun and moon data; tap page indicator to manage cities - **Sunrise & sunset times** with day/night length and countdown to next sunrise - **Animated sun/moon arc** -- a visual progress indicator showing where the sun (or moon) is in its path across the sky - **Moon phase & illumination** -- current lunar phase emoji and illumination percentage diff --git a/app/build.gradle.kts b/app/build.gradle.kts index be8cdc8..87d0363 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.sunzones" minSdk = 26 targetSdk = 35 - versionCode = 3 - versionName = "1.2" + versionCode = 4 + versionName = "1.3" } signingConfigs { diff --git a/app/src/main/java/com/sunzones/ui/main/MainScreen.kt b/app/src/main/java/com/sunzones/ui/main/MainScreen.kt index b77b360..0aaf030 100644 --- a/app/src/main/java/com/sunzones/ui/main/MainScreen.kt +++ b/app/src/main/java/com/sunzones/ui/main/MainScreen.kt @@ -37,6 +37,7 @@ import com.sunzones.R import com.sunzones.ui.addlocation.AddLocationSheet import com.sunzones.ui.main.animation.getTimeGradient import com.sunzones.ui.main.animation.lerpGradient +import com.sunzones.ui.main.components.CityListSheet import com.sunzones.ui.main.components.PageIndicator import com.sunzones.ui.main.components.SunCard import com.sunzones.ui.theme.NightBottom @@ -50,6 +51,7 @@ fun MainScreen( ) { val uiState by viewModel.uiState.collectAsState() var showAddSheet by remember { mutableStateOf(false) } + var showCityListSheet by remember { mutableStateOf(false) } // Entrance animation — runs once on screen load, shared by all cards val entranceProgress = remember { Animatable(0f) } @@ -132,10 +134,11 @@ fun MainScreen( PageIndicator( pageCount = uiState.locations.size, currentPage = pagerState.currentPage, + onClick = { showCityListSheet = true }, modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding() - .padding(bottom = 80.dp) + .padding(bottom = 120.dp) ) } @@ -162,4 +165,16 @@ fun MainScreen( ) } + if (showCityListSheet) { + CityListSheet( + locations = uiState.locations, + onDismiss = { showCityListSheet = false }, + onDelete = { viewModel.deleteLocation(it) }, + onReorder = { from, to -> viewModel.reorderLocations(from, to) }, + onAddClick = { + showCityListSheet = false + showAddSheet = true + } + ) + } } diff --git a/app/src/main/java/com/sunzones/ui/main/components/CityListSheet.kt b/app/src/main/java/com/sunzones/ui/main/components/CityListSheet.kt new file mode 100644 index 0000000..3aa5ec4 --- /dev/null +++ b/app/src/main/java/com/sunzones/ui/main/components/CityListSheet.kt @@ -0,0 +1,269 @@ +package com.sunzones.ui.main.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.DragHandle +import androidx.compose.material.icons.rounded.MyLocation +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.sunzones.R +import com.sunzones.domain.model.SunLocation +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CityListSheet( + locations: List, + onDismiss: () -> Unit, + onDelete: (Long) -> Unit, + onReorder: (fromIndex: Int, toIndex: Int) -> Unit, + onAddClick: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val haptic = LocalHapticFeedback.current + val density = LocalDensity.current + + val localList = remember { mutableStateListOf() } + LaunchedEffect(locations) { + localList.clear() + localList.addAll(locations) + } + + var draggedItemIndex by remember { mutableIntStateOf(-1) } + var dragOffsetY by remember { mutableFloatStateOf(0f) } + val itemHeight = with(density) { 56.dp.toPx() } + + var locationToDelete by remember { mutableStateOf(null) } + + if (locationToDelete != null) { + AlertDialog( + onDismissRequest = { locationToDelete = null }, + title = { Text(stringResource(R.string.delete_location_title)) }, + text = { Text(stringResource(R.string.delete_location_message, locationToDelete!!.name)) }, + confirmButton = { + TextButton( + onClick = { + locationToDelete?.let { location -> + val index = localList.indexOfFirst { it.id == location.id } + if (index >= 0) { + localList.removeAt(index) + } + onDelete(location.id) + } + locationToDelete = null + } + ) { + Text( + text = stringResource(R.string.delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { locationToDelete = null }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.manage_cities), + style = MaterialTheme.typography.titleLarge + ) + IconButton(onClick = onAddClick) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.add_location), + tint = MaterialTheme.colorScheme.primary + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (localList.isEmpty()) { + Text( + text = stringResource(R.string.empty_state), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 24.dp) + ) + } else { + val listState = rememberLazyListState() + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxWidth() + ) { + itemsIndexed( + items = localList, + key = { _, item -> item.id } + ) { index, location -> + val isDragging = draggedItemIndex == index + val elevation by animateDpAsState( + targetValue = if (isDragging) 8.dp else 0.dp, + label = "elevation" + ) + val backgroundColor by animateColorAsState( + targetValue = if (isDragging) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + MaterialTheme.colorScheme.surface + }, + label = "backgroundColor" + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .zIndex(if (isDragging) 1f else 0f) + .offset { + IntOffset( + x = 0, + y = if (isDragging) dragOffsetY.roundToInt() else 0 + ) + } + .shadow(elevation, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .background(backgroundColor) + .height(56.dp) + .padding(start = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.DragHandle, + contentDescription = stringResource(R.string.drag_to_reorder), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(24.dp) + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { + draggedItemIndex = index + dragOffsetY = 0f + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onDrag = { change, dragAmount -> + change.consume() + dragOffsetY += dragAmount.y + val targetIndex = (index + (dragOffsetY / itemHeight).roundToInt()) + .coerceIn(0, localList.size - 1) + if (targetIndex != index && targetIndex != draggedItemIndex) { + val item = localList.removeAt(draggedItemIndex) + localList.add(targetIndex, item) + dragOffsetY -= (targetIndex - draggedItemIndex) * itemHeight + draggedItemIndex = targetIndex + } + }, + onDragEnd = { + if (draggedItemIndex != index) { + onReorder(index, draggedItemIndex) + } + draggedItemIndex = -1 + dragOffsetY = 0f + }, + onDragCancel = { + draggedItemIndex = -1 + dragOffsetY = 0f + } + ) + } + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = location.name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + + if (location.isCurrentLocation) { + Icon( + imageVector = Icons.Rounded.MyLocation, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + + IconButton( + onClick = { locationToDelete = location } + ) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/sunzones/ui/main/components/PageIndicator.kt b/app/src/main/java/com/sunzones/ui/main/components/PageIndicator.kt index 93353fe..05dbd21 100644 --- a/app/src/main/java/com/sunzones/ui/main/components/PageIndicator.kt +++ b/app/src/main/java/com/sunzones/ui/main/components/PageIndicator.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -14,10 +13,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -36,11 +33,7 @@ fun PageIndicator( Box( modifier = modifier .clip(RoundedCornerShape(16.dp)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(color = Color.White.copy(alpha = 0.3f)), - onClick = onClick - ) + .clickable(onClick = onClick) .padding(horizontal = 12.dp, vertical = 8.dp) ) { Row( diff --git a/app/src/main/java/com/sunzones/ui/main/components/SunCard.kt b/app/src/main/java/com/sunzones/ui/main/components/SunCard.kt index 60cc63a..15706e7 100644 --- a/app/src/main/java/com/sunzones/ui/main/components/SunCard.kt +++ b/app/src/main/java/com/sunzones/ui/main/components/SunCard.kt @@ -1,14 +1,11 @@ package com.sunzones.ui.main.components -import android.content.Intent -import android.net.Uri import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween -import androidx.compose.foundation.clickable import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -40,8 +37,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -54,7 +49,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -87,7 +81,6 @@ fun SunCard( modifier: Modifier = Modifier ) { var showDeleteDialog by remember { mutableStateOf(false) } - var showAboutDialog by remember { mutableStateOf(false) } val scrollState = rememberScrollState() @@ -393,23 +386,6 @@ fun SunCard( modifier = Modifier.padding(top = 8.dp) ) - Spacer(modifier = Modifier.height(24.dp)) - - // Subtle about button - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.about_title), - style = MaterialTheme.typography.bodySmall, - color = TextOnDarkSecondary.copy(alpha = 0.35f), - modifier = Modifier - .clickable { showAboutDialog = true } - .padding(vertical = 12.dp, horizontal = 24.dp) - ) - } - // Space for navigation bar + page indicator + FAB Spacer( modifier = Modifier @@ -450,51 +426,6 @@ fun SunCard( ) } - if (showAboutDialog) { - val context = LocalContext.current - val email = stringResource(R.string.about_email) - Dialog( - onDismissRequest = { showAboutDialog = false }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Box( - modifier = Modifier - .padding(horizontal = 48.dp) - .background( - color = Color.Black.copy(alpha = 0.7f), - shape = RoundedCornerShape(28.dp) - ) - .padding(vertical = 32.dp, horizontal = 24.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "\u2600\uFE0F", - style = MaterialTheme.typography.displayLarge - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.about_made_in), - style = MaterialTheme.typography.bodyLarge, - color = Color.White - ) - Spacer(modifier = Modifier.height(16.dp)) - TextButton(onClick = { - context.startActivity( - Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$email")) - ) - }) { - Text( - text = stringResource(R.string.about_contact), - color = SunGold - ) - } - } - } - } - } } /** diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2e242ef..2967d6f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -12,10 +12,12 @@ Usunąć %1$s z Twoich lokalizacji? Usuń Anuluj - Daylight w roku + Długość dnia w roku O aplikacji Napisz do mnie OK Długość nocy Wschód za %1$s + Zarządzaj miastami + Przeciągnij by zmienić kolejność diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 926a861..625c5b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,6 @@ OK Night length Sunrise in %1$s + Manage Cities + Drag to reorder