Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f5e24224 | ||
|
|
4ce8d319a6 | ||
|
|
b9dc0d0f71 | ||
|
|
6154e09516 | ||
|
|
5493c78ec6 | ||
|
|
293a8f34ba | ||
|
|
14a462afba | ||
|
|
a2b0540403 | ||
|
|
575d44485b |
+222
@@ -0,0 +1,222 @@
|
||||
# Contributing to Flux
|
||||
|
||||
Thank you for your interest in contributing to Flux! Your support means a lot.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [How to Contribute](#how-to-contribute)
|
||||
- [Branch Naming](#branch-naming)
|
||||
- [Commit Messages](#commit-messages)
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Module Structure](#module-structure)
|
||||
- [Code Style & Conventions](#code-style--conventions)
|
||||
- [Tech Stack](#tech-stack)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the latest stable version of [Android Studio](https://developer.android.com/studio).
|
||||
2. Clone the repository:
|
||||
```bash
|
||||
git clone git@github.com:chindaronit/Flux.git
|
||||
```
|
||||
3. Open the project in Android Studio and let Gradle sync complete.
|
||||
4. Select `Run > Run 'app'` to build and launch the app on a device or emulator.
|
||||
|
||||
**Requirements:**
|
||||
- Android SDK target: **36**, minimum: **29** (Android 10+)
|
||||
- Kotlin: **2.2.10**
|
||||
- JVM Target: **11**
|
||||
- Build tool: **Gradle with Kotlin DSL**
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the Repository** — Fork to your own GitHub account.
|
||||
2. **Create a Branch** — Branch off from `main` using the naming conventions below.
|
||||
3. **Make Your Changes** — Follow the architecture and code style described in this guide.
|
||||
4. **Open an Issue First** — For non-trivial changes, create an issue before opening a PR so the approach can be discussed.
|
||||
5. **Submit a Pull Request** — Target the `dev` branch. Reference the related issue in the PR description.
|
||||
|
||||
---
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Use descriptive, hyphenated branch names prefixed by type:
|
||||
|
||||
| Type | Pattern | Example |
|
||||
|------|---------|---------|
|
||||
| Feature | `feat/<short-description>` | `feat/add-calendar-widget` |
|
||||
| Bug fix | `fix/<issue-or-description>` | `fix/resolve-issue-123` |
|
||||
| Refactor | `refactor/<description>` | `refactor/viewmodel-cleanup` |
|
||||
| Translation | `i18n/<language>` | `i18n/add-japanese` |
|
||||
| Documentation | `docs/<description>` | `docs/update-contributing` |
|
||||
|
||||
---
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Write concise, present-tense commit messages that describe *what* the change does:
|
||||
|
||||
```
|
||||
feat: add biometric lock toggle in settings
|
||||
fix: resolve crash when deleting last workspace
|
||||
refactor: extract reminder logic into ReminderReceiver
|
||||
i18n: add Spanish translation strings
|
||||
```
|
||||
|
||||
Avoid vague messages like `fix stuff`, `WIP`, or `update`.
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- Target the **`dev`** branch, not `master`/`main`.
|
||||
- Reference the issue your PR addresses (e.g., `Closes #42`).
|
||||
- Keep PRs focused — one feature or fix per PR.
|
||||
- Ensure the app builds and runs without errors before submitting.
|
||||
- Add or update string resources in `res/values/strings.xml` for any user-facing text.
|
||||
- If adding a new screen, register it in the navigation graph (`navigation/`).
|
||||
- If adding a new data entity, update the relevant DAO, model, repository, and DI module.
|
||||
- For new languages, add a corresponding `res/values-<lang>/strings.xml` file.
|
||||
|
||||
I'll review your pull request as soon as possible. Thank you for your contribution!
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Flux follows **MVI (Model–View–Intent)** architecture with **Jetpack Compose** for the UI layer. The data flow is strictly unidirectional:
|
||||
|
||||
```
|
||||
User Interaction (Intent / Event)
|
||||
↓
|
||||
ViewModel
|
||||
↓
|
||||
State Update
|
||||
↓
|
||||
Composable UI (re-renders from State)
|
||||
```
|
||||
|
||||
- **Model** — Data layer: Room entities, DAOs, repositories.
|
||||
- **View** — Composable screens that observe `State` objects and emit `Events`.
|
||||
- **Intent/ViewModel** — Processes events, calls repository methods, and emits new `State`.
|
||||
|
||||
Side effects (navigation, toasts, etc.) are handled via `Effects` (one-shot flows) separate from persistent UI state.
|
||||
|
||||
**Dependency injection** is provided by **Hilt**, keeping ViewModels and repositories decoupled and testable.
|
||||
|
||||
---
|
||||
|
||||
## Module Structure
|
||||
|
||||
All source code lives under `app/src/main/java/com/flux/`.
|
||||
|
||||
```
|
||||
app/src/main/java/com/flux/
|
||||
│
|
||||
├── data/ # Data layer
|
||||
│ ├── dao/ # Room DAO interfaces (one per entity)
|
||||
│ ├── models/ # Room @Entity data classes and related data models
|
||||
│ ├── repository/ # Repository implementations (single source of truth)
|
||||
│ └── database/ # Room database definition and migrations
|
||||
│
|
||||
├── di/ # Hilt dependency injection modules
|
||||
│ ├── DataModules.kt # Provides database and DAO instances
|
||||
│ ├── RepositoryModules.kt # Binds repository interfaces to implementations
|
||||
│ └── Flux.kt # @HiltAndroidApp application class
|
||||
│
|
||||
├── navigation/ # Compose Navigation graph and route definitions
|
||||
│
|
||||
├── other/ # Utility and system-integration classes
|
||||
│ ├── utils/ # General-purpose helper functions and extensions
|
||||
│ ├── BackupManager.kt # Handles data backup and restore logic
|
||||
│ ├── ReminderReceiver.kt # BroadcastReceiver for scheduled reminders
|
||||
│ ├── BootReceiver.kt # Reschedules reminders after device reboot
|
||||
│ ├── workspaceIcons/ # Icon assets and mapping utilities for workspaces
|
||||
│ ├── BiometricAuthentication.kt # Biometric lock integration
|
||||
│ └── [other app-wide utilities]
|
||||
│
|
||||
├── ui/ # UI layer (Jetpack Compose)
|
||||
│ ├── common/ # Shared, reusable composables (buttons, dialogs, cards, etc.)
|
||||
│ ├── events/ # Sealed classes / data classes representing user intents per screen
|
||||
│ ├── states/ # Data classes holding observable UI state per screen
|
||||
│ ├── effects/ # One-shot side-effect flows (navigation, snackbars, etc.)
|
||||
│ ├── screens/ # Full-screen composables, one file per screen
|
||||
│ ├── viewModels/ # Hilt-injected ViewModels; process events, expose state
|
||||
│ └── theme/ # Material 3 color scheme, typography, and shape definitions
|
||||
│
|
||||
└── MainActivity.kt # Single-activity entry point; hosts NavHost
|
||||
```
|
||||
|
||||
```
|
||||
app/src/main/res/
|
||||
│
|
||||
├── drawable/ # Vector drawables and icons
|
||||
└── values/ # Resource files
|
||||
├── strings.xml # Default (English) strings
|
||||
└── values-<lang>/ # Translations (hi, fr, pt-BR, ru, de, es, nl, zh-rCN, …)
|
||||
```
|
||||
|
||||
### Adding a New Feature — Checklist
|
||||
|
||||
When building a new screen or feature, create/update files in this order:
|
||||
|
||||
1. **`data/models/`** — Define the Room entity or data class.
|
||||
2. **`data/dao/`** — Write the DAO interface with required queries.
|
||||
3. **`data/database/`** — Add the entity to the database and increment the version with a migration.
|
||||
4. **`data/repository/`** — Implement the repository exposing `Flow`s and suspend functions.
|
||||
5. **`di/`** — Bind the new DAO/repository in the appropriate Hilt module.
|
||||
6. **`ui/events/`** — Define a sealed class for all user actions on the new screen.
|
||||
7. **`ui/states/`** — Define the data class representing the full UI state for the screen.
|
||||
8. **`ui/viewModels/`** — Implement the ViewModel; inject the repository via Hilt.
|
||||
9. **`ui/screens/`** — Build the Composable screen; collect state, dispatch events.
|
||||
10. **`navigation/`** — Register the new route and composable in the nav graph.
|
||||
11. **`res/values/strings.xml`** — Add all user-facing strings; keep zero hardcoded text in composables.
|
||||
|
||||
---
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
- **Language**: Kotlin only. No Java files.
|
||||
- **Formatting**: Follow standard Kotlin style (4-space indent, no wildcard imports).
|
||||
- **Naming**:
|
||||
- ViewModels: `<Feature>ViewModel` (e.g., `NoteViewModel`)
|
||||
- States: `<Feature>State` (e.g., `NoteState`)
|
||||
- Events: `<Feature>Event` (e.g., `NoteEvent`)
|
||||
- Effects: `<Feature>Effect` (e.g., `NoteEffect`)
|
||||
- Screens: `<Feature>Screen` (e.g., `NoteScreen`)
|
||||
- DAOs: `<Entity>Dao` (e.g., `NoteDao`)
|
||||
- **State hoisting**: Keep composables stateless where possible; hoist state to the ViewModel.
|
||||
- **No business logic in composables**: Composables should only render state and forward events.
|
||||
- **Strings**: All user-visible text must live in `strings.xml`. Never hardcode strings in composables.
|
||||
- **Markdown content**: Flux uses CommonMark + GitHub Flavored Markdown (GFM) with LaTeX math support. Refer to `Guide.md` for supported syntax.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Language | Kotlin 2.2.10 |
|
||||
| UI | Jetpack Compose + Material 3 (Material You) |
|
||||
| Architecture | MVI + ViewModel |
|
||||
| DI | Hilt |
|
||||
| Database | Room (SQLite abstraction) |
|
||||
| Navigation | Compose Navigation |
|
||||
| Markdown | CommonMark + Flexmark (HTML→MD) |
|
||||
| Math | LaTeX via inline renderer |
|
||||
| Async | Kotlin Coroutines + Flow |
|
||||
| Build | Gradle Kotlin DSL |
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
Open a [Discussion](https://github.com/chindaronit/Flux/discussions) or file an [Issue](https://github.com/chindaronit/Flux/issues) — happy to help you get oriented.
|
||||
@@ -40,23 +40,25 @@
|
||||
|
||||
## 🎉 Features
|
||||
|
||||
- 📝 **Sleek, Minimalistic Design** with Material You (Material 3)
|
||||
- 🌟 **Workspace Templates**: Organize tasks with custom workspaces
|
||||
- 🔐 **Biometric App Lock** for privacy
|
||||
- 🚀 **Frequent Updates & Improvements**
|
||||
|
||||
---
|
||||
|
||||
## 💬 Contact Me
|
||||
|
||||
- 📧 **Email:** ronitchinda100@gmail.com
|
||||
- 📸 **Instagram:** [@chinda.ronit](https://www.instagram.com/chinda_ronit/)
|
||||
* ✨ **Modern Material You Design** built with Material 3 for a clean and intuitive experience.
|
||||
* 🗂️ **Flexible Workspaces** with customizable templates to organize notes, tasks, journals, habits, and events according to your workflow.
|
||||
* 📝 **Rich Markdown Editor** with support for formatting, code blocks, tables, LaTeX, Mermaid diagrams, and more.
|
||||
* ✅ **Advanced Task Management** with recurring tasks, reminders, progress tracking, and completion analytics.
|
||||
* 📔 **Daily Journals & Notes** designed for capturing ideas, reflections, and long-form content.
|
||||
* 📅 **Integrated Calendar & Events** to manage schedules and important dates in one place.
|
||||
* 🔥 **Habit Tracking & Analytics** with insights, statistics, and visual progress monitoring.
|
||||
* 🖼️ **Media Support** for attaching images and organizing content alongside text.
|
||||
* 🔒 **Privacy-Focused Security** with optional biometric app lock and screen protection.
|
||||
* 📴 **Offline-First Architecture** powered by local storage for fast and reliable access.
|
||||
* 🎨 **Highly Customizable Experience** with themes, workspace organization, and personalized layouts.
|
||||
* 🌍 **Open Source & Community Driven** under GPL-3.0 with frequent updates and continuous improvements.
|
||||
* 🚀 **Built with Jetpack Compose** for a fast, smooth, and native Android experience.
|
||||
|
||||
---
|
||||
|
||||
## 🌎 Translations
|
||||
|
||||
English, Hindi, French, Portugal (Brazil), Russian, German, Spanish, Dutch
|
||||
English, Hindi, French, Portugal (Brazil), Russian, German, Spanish, Dutch, Chinese (Simple)
|
||||
|
||||
## 🔎 Technical Details
|
||||
|
||||
@@ -64,7 +66,7 @@ English, Hindi, French, Portugal (Brazil), Russian, German, Spanish, Dutch
|
||||
- **Build Tool**: Gradle with Kotlin DSL
|
||||
- **Android Version**: The application targets Android SDK version 36 and is compatible with devices
|
||||
running Android SDK version 29 and above.
|
||||
- **Kotlin Version**: 2.2.10.
|
||||
- **Kotlin Version**: 2.4.0
|
||||
- **Java Version**: JVM Target 11.
|
||||
|
||||
## 🛠️ Architecture
|
||||
@@ -73,15 +75,19 @@ English, Hindi, French, Portugal (Brazil), Russian, German, Spanish, Dutch
|
||||
|
||||
## 📚 Libraries and Frameworks
|
||||
|
||||
- **Compose**: A modern toolkit for building native Android UI.
|
||||
- **Hilt**: A dependency injection library for Android.
|
||||
- **KSP (Kotlin Symbol Processing API)**: Enhances Kotlin compilation with additional metadata
|
||||
processing.
|
||||
- **Room**: A persistence library providing an abstraction layer over SQLite.
|
||||
- **Compose Navigation**: Simplifies the implementation of navigation between screens.
|
||||
- **Material Icons**: Provides Material Design icons for consistent visual elements.
|
||||
- **CommonMark**: For markdown parsing and rendering.
|
||||
- **Flexmark HTML to Markdown Converter**: Converts HTML to Markdown.
|
||||
- 🎨 Modern Material You Interface — Material 3 design language, clean and intuitive UX.
|
||||
- 🗂️ Customizable Workspaces — templates for notes, tasks, journals, habits, and events.
|
||||
- 📝 Powerful Markdown Editor — tables, code blocks, LaTeX equations, Mermaid diagrams, and advanced formatting.
|
||||
- 📋 Comprehensive Task Management — recurring tasks, reminders, progress tracking, and completion analytics.
|
||||
- 📔 Notes & Journaling — capture ideas, daily reflections, and long-form content.
|
||||
- 📅 Calendar & Event Planning — manage schedules, deadlines, and important dates.
|
||||
- 📊 Habit Tracking & Insights — statistics and visual progress analytics.
|
||||
- 🖼️ Media Support — attach and organize images alongside your content.
|
||||
- 🔐 Privacy-First Design — biometric app lock and screen protection.
|
||||
- ✈️ Offline-First Architecture — fast, reliable access without requiring an internet connection.
|
||||
- ⚙️ Personalized Experience — theme customization and flexible workspace organization.
|
||||
- 🔓 Open Source — licensed under GPL-3.0, with active development and regular updates.
|
||||
- 🤖 Built with Jetpack Compose — smooth, modern, native Android experience.
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
@@ -101,7 +107,9 @@ an issue. If you want to contribute code directly to this project, you can creat
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Thanks to all contributors
|
||||
## Credits.
|
||||
|
||||
[](https://starchart.cc/chindaronit/Flux)
|
||||
|
||||
<a href="https://github.com/chindaronit/Flux/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=chindaronit/Flux" width="200"/>
|
||||
|
||||
@@ -8,14 +8,14 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.flux"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.flux"
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
versionCode = 11
|
||||
versionName = "3.1.5"
|
||||
targetSdk = 37
|
||||
versionCode = 14
|
||||
versionName = "3.1.8"
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@@ -106,6 +106,7 @@ dependencies {
|
||||
|
||||
// Hilt
|
||||
ksp(libs.hilt.android.compiler)
|
||||
ksp(libs.kotlinMetadataWorkaround)
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
|
||||
@@ -121,7 +122,7 @@ dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// CommonMark, for markdown rendering and parsing
|
||||
// CommonMark, for Markdown rendering and parsing
|
||||
implementation(libs.commonmark.ext.autolink)
|
||||
implementation(libs.commonmark.ext.footnotes)
|
||||
implementation(libs.commonmark.ext.ins)
|
||||
@@ -133,4 +134,7 @@ dependencies {
|
||||
implementation(libs.commonmark.ext.yaml.front.matter)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.flexmarkHtml2mdConverter)
|
||||
|
||||
// draggable list
|
||||
implementation(libs.reorderable)
|
||||
}
|
||||
|
||||
@@ -146,12 +146,39 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Async CSS loading -->
|
||||
<link rel="stylesheet" href="file:///android_asset/katex/katex.min.css">
|
||||
<link rel="stylesheet" href="file:///android_asset/prism/prism-theme-light.css"
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="file:///android_asset/prism/prism-theme-light.css"
|
||||
id="prism-light-theme">
|
||||
<link rel="stylesheet" href="file:///android_asset/prism/prism-theme-dark.css"
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="file:///android_asset/prism/prism-theme-dark.css"
|
||||
id="prism-dark-theme">
|
||||
|
||||
<style>
|
||||
pre {
|
||||
background-color: {{PRE_BACKGROUND}} !important;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
background-color: {{PRE_BACKGROUND}} !important;
|
||||
color: {{TEXT_COLOR}} !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:not(pre) > code,
|
||||
:not(pre) > code[class*="language-"] {
|
||||
background-color: {{CODE_BACKGROUND}} !important;
|
||||
color: {{TEXT_COLOR}} !important;
|
||||
padding: 4px 4px 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>{{CONTENT}}</main>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.flux.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.flux.data.model.TodoInstance
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TodoInstanceDao {
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM TodoInstance WHERE todoId = :todoId and instanceDate = :instanceDate)")
|
||||
suspend fun exists(todoId: String, instanceDate: Long): Boolean
|
||||
|
||||
@Query("SELECT * FROM TodoInstance")
|
||||
fun loadAll(): Flow<List<TodoInstance>>
|
||||
|
||||
@Query("SELECT * FROM TodoInstance")
|
||||
fun loadAllInstances(): List<TodoInstance>
|
||||
|
||||
@Query("DELETE FROM TodoInstance WHERE workspaceId = :workspaceId")
|
||||
suspend fun deleteAllWorkspaceInstance(workspaceId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertTodoInstance(instance: TodoInstance)
|
||||
|
||||
@Query("DELETE FROM TodoInstance WHERE todoId = :listId")
|
||||
suspend fun deleteListInstances(listId: String)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.ProgressBoardModel
|
||||
import com.flux.data.model.SettingsModel
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -18,6 +19,7 @@ data class FluxBackup(
|
||||
val workspaces: List<WorkspaceModel> = emptyList(),
|
||||
val notes: List<NotesModel> = emptyList(),
|
||||
val todos: List<TodoModel> = emptyList(),
|
||||
val todoInstances: List<TodoInstance> = emptyList(),
|
||||
val habits: List<HabitModel> = emptyList(),
|
||||
val habitInstances: List<HabitInstanceModel> = emptyList(),
|
||||
val journals: List<JournalModel> = emptyList(),
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.flux.data.dao.NotesDao
|
||||
import com.flux.data.dao.ProgressBoardDao
|
||||
import com.flux.data.dao.SettingsDao
|
||||
import com.flux.data.dao.TodoDao
|
||||
import com.flux.data.dao.TodoInstanceDao
|
||||
import com.flux.data.dao.WorkspaceDao
|
||||
import com.flux.data.model.Converter
|
||||
import com.flux.data.model.EventInstanceModel
|
||||
@@ -26,6 +27,7 @@ import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.ProgressBoardModel
|
||||
import com.flux.data.model.SettingsModel
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.google.gson.Gson
|
||||
@@ -35,8 +37,8 @@ import kotlinx.serialization.json.Json
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [EventModel::class, LabelModel::class, EventInstanceModel::class, SettingsModel::class, NotesModel::class, HabitModel::class, HabitInstanceModel::class, WorkspaceModel::class, TodoModel::class, JournalModel::class, ProgressBoardModel::class],
|
||||
version = 8,
|
||||
entities = [EventModel::class, LabelModel::class, EventInstanceModel::class, SettingsModel::class, NotesModel::class, HabitModel::class, HabitInstanceModel::class, WorkspaceModel::class, TodoModel::class, JournalModel::class, ProgressBoardModel::class, TodoInstance::class],
|
||||
version = 10,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converter::class)
|
||||
@@ -52,6 +54,7 @@ abstract class FluxDatabase : RoomDatabase() {
|
||||
abstract val todoDao: TodoDao
|
||||
abstract val labelDao: LabelDao
|
||||
abstract val progressBoardDao: ProgressBoardDao
|
||||
abstract val todoInstanceDao: TodoInstanceDao
|
||||
}
|
||||
|
||||
private fun SupportSQLiteDatabase.safeExec(sql: String) {
|
||||
@@ -347,4 +350,66 @@ val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||
if (!db.columnExists("JournalModel", "labels"))
|
||||
db.safeExec("ALTER TABLE JournalModel ADD COLUMN labels TEXT NOT NULL DEFAULT '[]'")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_8_9 = object : Migration(8, 9) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
|
||||
// 1. Create corrected TodoModel table with proper constraints
|
||||
db.safeExec("""
|
||||
CREATE TABLE TodoModel_new (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
workspaceId TEXT NOT NULL DEFAULT '',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
items TEXT NOT NULL DEFAULT '[]',
|
||||
startDateTime INTEGER NOT NULL DEFAULT ${System.currentTimeMillis()},
|
||||
recurrence TEXT NOT NULL DEFAULT '{"type":"NONE"}'
|
||||
)
|
||||
""".trimIndent())
|
||||
|
||||
// 2. Copy existing data, coercing any nulls
|
||||
db.safeExec("""
|
||||
INSERT INTO TodoModel_new (id, workspaceId, title, items, startDateTime, recurrence)
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(workspaceId, ''),
|
||||
COALESCE(title, ''),
|
||||
COALESCE(items, '[]'),
|
||||
${System.currentTimeMillis()},
|
||||
'{"type":"NONE"}'
|
||||
FROM TodoModel
|
||||
""".trimIndent())
|
||||
|
||||
// 3. Swap tables
|
||||
db.safeExec("DROP TABLE TodoModel")
|
||||
db.safeExec("ALTER TABLE TodoModel_new RENAME TO TodoModel")
|
||||
|
||||
// 4. Create TodoInstance table
|
||||
if (!db.tableExists("TodoInstance")) {
|
||||
db.safeExec("""
|
||||
CREATE TABLE TodoInstance (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
todoId TEXT NOT NULL,
|
||||
instanceDate INTEGER NOT NULL,
|
||||
workspaceId TEXT NOT NULL,
|
||||
items TEXT NOT NULL DEFAULT '[]',
|
||||
FOREIGN KEY (todoId) REFERENCES TodoModel(id) ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
// 5. Create unique index on TodoInstance
|
||||
db.safeExec("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS
|
||||
index_TodoInstance_todoId_instanceDate
|
||||
ON TodoInstance(todoId, instanceDate)
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
if (!db.columnExists("SettingsModel", "notesPreviewMode"))
|
||||
db.safeExec("ALTER TABLE SettingsModel ADD COLUMN notesPreviewMode INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
@@ -73,5 +73,7 @@ fun EventModel.occursOn(date: LocalDate): Boolean {
|
||||
date.dayOfMonth == eventStart.dayOfMonth &&
|
||||
date.month == eventStart.month
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class RecurrenceRule {
|
||||
@Serializable
|
||||
object NONE: RecurrenceRule()
|
||||
|
||||
@Serializable
|
||||
object Once : RecurrenceRule()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Intent
|
||||
import com.flux.other.ReminderReceiver
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
enum class ReminderType { EVENT, HABIT }
|
||||
enum class ReminderType { EVENT, HABIT, TODO }
|
||||
|
||||
data class ScheduleRequest(
|
||||
val itemId: String,
|
||||
@@ -44,13 +44,6 @@ data class ScheduleRequest(
|
||||
}
|
||||
}
|
||||
|
||||
private const val DAY_MILLIS =
|
||||
24L * 60L * 60L * 1000L
|
||||
|
||||
private fun Int.minutesToMillis(): Long {
|
||||
return this * 60L * 1000L
|
||||
}
|
||||
|
||||
fun HabitModel.toScheduleRequest() = ScheduleRequest(
|
||||
itemId = id,
|
||||
itemType = ReminderType.HABIT,
|
||||
@@ -76,6 +69,18 @@ fun EventModel.toScheduleRequest() = ScheduleRequest(
|
||||
workspaceId = workspaceId
|
||||
)
|
||||
|
||||
fun TodoModel.toScheduleRequest() = ScheduleRequest(
|
||||
itemId = id,
|
||||
itemType = ReminderType.TODO,
|
||||
title = title,
|
||||
description = "",
|
||||
recurrence = recurrence,
|
||||
startDateTime = startDateTime,
|
||||
endDateTime = -1L,
|
||||
notificationOffset = 0L,
|
||||
workspaceId = workspaceId
|
||||
)
|
||||
|
||||
fun ScheduleRequest.toIntent(context: Context): Intent {
|
||||
return Intent(context, ReminderReceiver::class.java).apply {
|
||||
putExtra("itemId", itemId)
|
||||
|
||||
@@ -28,5 +28,6 @@ data class SettingsModel(
|
||||
val isLineNumbersVisible: Boolean = false,
|
||||
val startWithReadView: Boolean = false,
|
||||
val storageRootUri: String? = null,
|
||||
val backupFrequency: Int = 0
|
||||
val backupFrequency: Int = 0,
|
||||
val notesPreviewMode: Int = 1
|
||||
)
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package com.flux.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDate
|
||||
|
||||
@Serializable
|
||||
@Entity
|
||||
data class TodoModel(
|
||||
@PrimaryKey
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val workspaceId: String ="",
|
||||
val workspaceId: String = "",
|
||||
val title: String = "",
|
||||
val items: List<TodoItem> = emptyList()
|
||||
val items: List<TodoItem> = emptyList(),
|
||||
val startDateTime: Long = System.currentTimeMillis(),
|
||||
val recurrence: RecurrenceRule = RecurrenceRule.NONE,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -20,4 +25,187 @@ data class TodoItem(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val value: String = "",
|
||||
val isChecked: Boolean = false
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = TodoModel::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["todoId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [
|
||||
Index(
|
||||
value = ["todoId", "instanceDate"],
|
||||
unique = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class TodoInstance(
|
||||
@PrimaryKey
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val todoId: String = "",
|
||||
val workspaceId: String="",
|
||||
val instanceDate: Long = LocalDate.now().toEpochDay(),
|
||||
val items: List<TodoItem> = emptyList()
|
||||
)
|
||||
|
||||
fun TodoInstance.isCompleted(): Boolean {
|
||||
return items.all { it.isChecked }
|
||||
}
|
||||
|
||||
fun TodoModel.toHtml(): String {
|
||||
val itemsHtml = items.joinToString("\n") { item ->
|
||||
val checkbox = if (item.isChecked) "☑" else "☐"
|
||||
|
||||
"""
|
||||
<div class="todo-item">
|
||||
<span class="checkbox">$checkbox</span>
|
||||
<span>${item.value}</span>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 32px;
|
||||
color: black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$title</h1>
|
||||
|
||||
$itemsHtml
|
||||
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun TodoInstance.toHtml(title: String): String {
|
||||
val itemsHtml = items.joinToString("\n") { item ->
|
||||
val checkbox = if (item.isChecked) "☑" else "☐"
|
||||
|
||||
"""
|
||||
<div class="todo-item">
|
||||
<span class="checkbox">$checkbox</span>
|
||||
<span class="text">${item.value}</span>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>
|
||||
html {
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
body {
|
||||
display: inline-block;
|
||||
width: max-content;
|
||||
margin: 0;
|
||||
padding: 32px;
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 24px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>$title</h1>
|
||||
|
||||
$itemsHtml
|
||||
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
// Title is excluded
|
||||
fun TodoModel.toMarkdownContent(): String {
|
||||
return buildString {
|
||||
items.forEach { item ->
|
||||
val check = if (item.isChecked) "x" else " "
|
||||
append("- [$check] ${item.value}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun TodoModel.toMarkdown(): String {
|
||||
return buildString {
|
||||
append("# $title\n\n")
|
||||
|
||||
items.forEach { item ->
|
||||
val check = if (item.isChecked) "x" else " "
|
||||
append("- [$check] ${item.value}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TodoModel.toText(): String {
|
||||
return buildString {
|
||||
append("# $title\n\n")
|
||||
|
||||
items.forEach { item ->
|
||||
val check = if (item.isChecked) "☑" else "☐"
|
||||
append("- $check ${item.value}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.flux.data.repository
|
||||
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -7,5 +8,10 @@ interface TodoRepository {
|
||||
suspend fun upsertList(list: TodoModel)
|
||||
suspend fun deleteList(list: TodoModel)
|
||||
suspend fun deleteAllWorkspaceLists(workspaceId: String)
|
||||
suspend fun upsertInstance(todoInstance: TodoInstance)
|
||||
suspend fun deleteAllWorkspaceInstance(workspaceId: String)
|
||||
suspend fun existInstance(listId: String, instanceDate: Long): Boolean
|
||||
suspend fun loadAllLists(): List<TodoModel>
|
||||
fun loadAllTodoInstance(): Flow<List<TodoInstance>>
|
||||
fun loadTodoData(): Flow<List<TodoModel>>
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.flux.data.repository
|
||||
|
||||
import com.flux.data.dao.TodoDao
|
||||
import com.flux.data.dao.TodoInstanceDao
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -8,7 +10,8 @@ import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class TodoRepositoryImpl @Inject constructor(
|
||||
val dao: TodoDao
|
||||
val dao: TodoDao,
|
||||
val instanceDao: TodoInstanceDao
|
||||
) : TodoRepository {
|
||||
override fun loadTodoData(): Flow<List<TodoModel>> {
|
||||
return dao.loadTodoData()
|
||||
@@ -19,10 +22,33 @@ class TodoRepositoryImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun deleteList(list: TodoModel) {
|
||||
return withContext(Dispatchers.IO) { dao.deleteList(list) }
|
||||
return withContext(Dispatchers.IO) {
|
||||
instanceDao.deleteListInstances(list.id)
|
||||
dao.deleteList(list)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteAllWorkspaceLists(workspaceId: String) {
|
||||
return withContext(Dispatchers.IO) { dao.deleteAllWorkspaceLists(workspaceId) }
|
||||
}
|
||||
|
||||
override suspend fun upsertInstance(todoInstance: TodoInstance) {
|
||||
return withContext(Dispatchers.IO) { instanceDao.upsertTodoInstance(todoInstance) }
|
||||
}
|
||||
|
||||
override suspend fun deleteAllWorkspaceInstance(workspaceId: String) {
|
||||
return withContext(Dispatchers.IO) { instanceDao.deleteAllWorkspaceInstance(workspaceId) }
|
||||
}
|
||||
|
||||
override suspend fun existInstance(listId: String, instanceDate: Long): Boolean {
|
||||
return withContext(Dispatchers.IO) { instanceDao.exists(listId, instanceDate) }
|
||||
}
|
||||
|
||||
override suspend fun loadAllLists(): List<TodoModel> {
|
||||
return withContext(Dispatchers.IO) { dao.loadAllLists() }
|
||||
}
|
||||
|
||||
override fun loadAllTodoInstance(): Flow<List<TodoInstance>> {
|
||||
return instanceDao.loadAll()
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import com.flux.data.dao.NotesDao
|
||||
import com.flux.data.dao.ProgressBoardDao
|
||||
import com.flux.data.dao.SettingsDao
|
||||
import com.flux.data.dao.TodoDao
|
||||
import com.flux.data.dao.TodoInstanceDao
|
||||
import com.flux.data.dao.WorkspaceDao
|
||||
import com.flux.data.database.FluxDatabase
|
||||
import com.flux.data.database.MIGRATION_1_2
|
||||
@@ -22,6 +23,8 @@ import com.flux.data.database.MIGRATION_4_5
|
||||
import com.flux.data.database.MIGRATION_5_6
|
||||
import com.flux.data.database.MIGRATION_6_7
|
||||
import com.flux.data.database.MIGRATION_7_8
|
||||
import com.flux.data.database.MIGRATION_8_9
|
||||
import com.flux.data.database.MIGRATION_9_10
|
||||
import com.flux.other.BackupManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -48,7 +51,9 @@ object DataModule {
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6,
|
||||
MIGRATION_6_7,
|
||||
MIGRATION_7_8
|
||||
MIGRATION_7_8,
|
||||
MIGRATION_8_9,
|
||||
MIGRATION_9_10
|
||||
)
|
||||
.build()
|
||||
|
||||
@@ -88,6 +93,10 @@ object DataModule {
|
||||
@Provides
|
||||
fun provideTodoDao(db: FluxDatabase): TodoDao = db.todoDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTodoInstanceDao(db: FluxDatabase): TodoInstanceDao = db.todoInstanceDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLabelDao(db: FluxDatabase): LabelDao = db.labelDao
|
||||
|
||||
@@ -27,10 +27,12 @@ import com.flux.ui.screens.settings.Data
|
||||
import com.flux.ui.screens.settings.Editor
|
||||
import com.flux.ui.screens.settings.Languages
|
||||
import com.flux.ui.screens.settings.Mode
|
||||
import com.flux.ui.screens.settings.NotesPreviewSetting
|
||||
import com.flux.ui.screens.settings.Privacy
|
||||
import com.flux.ui.screens.settings.Settings
|
||||
import com.flux.ui.screens.settings.StorageSelectionScreen
|
||||
import com.flux.ui.screens.settings.Themes
|
||||
import com.flux.ui.screens.todo.NewTodoList
|
||||
import com.flux.ui.screens.todo.TodoDetail
|
||||
import com.flux.ui.screens.workspaces.NewWorkspaceScreen
|
||||
import com.flux.ui.screens.workspaces.WorkspaceDetails
|
||||
@@ -50,6 +52,7 @@ sealed class NavRoutes(val route: String) {
|
||||
data object NewHabit : NavRoutes("workspace/habit/new") // new habit
|
||||
data object EventDetails : NavRoutes("workspace/event/details") // event detail
|
||||
data object TodoDetail : NavRoutes("workspace/todo/details") // TodoList
|
||||
data object NewTodoList : NavRoutes("workspace/todo/newTodo") // TodoList
|
||||
data object EditJournal : NavRoutes("workspace/journal/edit") // Journal
|
||||
data object NewEvent : NavRoutes("workspace/event/edit") // new event
|
||||
data object Analytics : NavRoutes("Analytics")
|
||||
@@ -66,6 +69,7 @@ sealed class NavRoutes(val route: String) {
|
||||
data object Backup : NavRoutes("setting/backup")
|
||||
data object Editor : NavRoutes("setting/editor")
|
||||
data object Mode : NavRoutes("setting/mode")
|
||||
data object NotesPreview : NavRoutes("setting/editor/notesPreview")
|
||||
|
||||
fun withArgs(vararg args: Any): String {
|
||||
return buildString {
|
||||
@@ -101,6 +105,7 @@ val NotesScreens =
|
||||
NavRoutes.NoteDetails.route + "/{workspaceId}" + "/{notesId}" to { navController, notesId, workspaceId, states, viewModel ->
|
||||
NoteDetails(
|
||||
navController,
|
||||
states.workspaceState.allWorkspaces,
|
||||
states.notesState.outline,
|
||||
states.notesState.textState,
|
||||
workspaceId,
|
||||
@@ -108,13 +113,15 @@ val NotesScreens =
|
||||
states.settings.data.isLintValid,
|
||||
states.settings.data.isLineNumbersVisible,
|
||||
states.settings.data.startWithReadView,
|
||||
states.notesState.allNotes.find { it.notesId == notesId }
|
||||
?: NotesModel(workspaceId = workspaceId),
|
||||
states.notesState.allNotes.find { it.notesId == notesId } ?: NotesModel(workspaceId = workspaceId),
|
||||
states.settings.data.storageRootUri,
|
||||
states.labelState.allLabels.filter { it.workspaceId==workspaceId },
|
||||
viewModel.settingsViewModel,
|
||||
viewModel.notesViewModel,
|
||||
viewModel.notesViewModel::onEvent
|
||||
viewModel.notesViewModel::onEvent,
|
||||
viewModel.journalViewModel::onEvent,
|
||||
viewModel.todoViewModel::onEvent,
|
||||
viewModel.workspaceViewModel::onEvent
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -127,8 +134,10 @@ val HabitScreens =
|
||||
states.settings.data.cornerRadius,
|
||||
workspaceId,
|
||||
states.habitState.allHabits.first { it.id == habitId },
|
||||
states.workspaceState.allWorkspaces,
|
||||
states.habitState.allInstances.filter { it.habitId == habitId },
|
||||
viewModel.habitViewModel::onEvent
|
||||
viewModel.habitViewModel::onEvent,
|
||||
viewModel.workspaceViewModel::onEvent
|
||||
)
|
||||
},
|
||||
NavRoutes.NewHabit.route + "/{workspaceId}" + "/{habitId}" to { navController, habitId, workspaceId, states, viewModel ->
|
||||
@@ -146,8 +155,22 @@ val TodoScreens =
|
||||
NavRoutes.TodoDetail.route + "/{workspaceId}" + "/{listId}" to { navController, listId, workspaceId, states, viewModel ->
|
||||
TodoDetail(
|
||||
navController,
|
||||
states.todoState.allLists.find { it.id == listId }
|
||||
?: TodoModel(workspaceId = workspaceId),
|
||||
states.settings.data.cornerRadius,
|
||||
states.todoState.allLists.first { it.id==listId },
|
||||
states.workspaceState.allWorkspaces,
|
||||
states.todoState.allInstances.filter { it.workspaceId==workspaceId && it.todoId==listId },
|
||||
workspaceId,
|
||||
viewModel.todoViewModel::onEvent,
|
||||
viewModel.notesViewModel::onEvent,
|
||||
viewModel.journalViewModel::onEvent,
|
||||
viewModel.workspaceViewModel::onEvent
|
||||
)
|
||||
},
|
||||
NavRoutes.NewTodoList.route + "/{workspaceId}" + "/{listId}" to { navController, listId, workspaceId, states, viewModel ->
|
||||
NewTodoList(
|
||||
navController,
|
||||
states.settings.data.is24HourFormat,
|
||||
states.todoState.allLists.find { it.id == listId } ?: TodoModel(workspaceId = workspaceId),
|
||||
workspaceId,
|
||||
viewModel.todoViewModel::onEvent
|
||||
)
|
||||
@@ -159,6 +182,8 @@ val JournalScreens =
|
||||
NavRoutes.EditJournal.route + "/{workspaceId}" + "/{journalId}" + "/{journalDateTime}" to { navController, journalId, journalDateTime, workspaceId, states, viewModel ->
|
||||
EditJournal(
|
||||
navController,
|
||||
states.workspaceState.allWorkspaces,
|
||||
workspaceId,
|
||||
states.journalState.data.find { it.journalId == journalId } ?: JournalModel(workspaceId = workspaceId, dateTime = journalDateTime),
|
||||
states.journalState.outline,
|
||||
states.journalState.textState,
|
||||
@@ -170,7 +195,10 @@ val JournalScreens =
|
||||
states.labelState.allLabels.filter { it.workspaceId==workspaceId },
|
||||
viewModel.journalViewModel,
|
||||
viewModel.settingsViewModel,
|
||||
viewModel.journalViewModel::onEvent
|
||||
viewModel.journalViewModel::onEvent,
|
||||
viewModel.notesViewModel::onEvent,
|
||||
viewModel.todoViewModel::onEvent,
|
||||
viewModel.workspaceViewModel::onEvent
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -207,6 +235,9 @@ val SettingsScreens =
|
||||
},
|
||||
NavRoutes.Mode.route to { navController, _, states, viewModels ->
|
||||
Mode(navController, states.settings, viewModels.settingsViewModel::onEvent)
|
||||
},
|
||||
NavRoutes.NotesPreview.route to { navController, _, states, viewModels ->
|
||||
NotesPreviewSetting(navController, states.settings, viewModels.settingsViewModel::onEvent)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -215,12 +246,14 @@ val EventScreens =
|
||||
NavRoutes.EventDetails.route + "/{workspaceId}" + "/{eventId}" + "/{instanceDate}" to { navController, states, viewModels, eventId, workspaceId, instanceDate, _ ->
|
||||
EventDetails(
|
||||
navController,
|
||||
states.workspaceState.allWorkspaces,
|
||||
workspaceId,
|
||||
states.eventState.allEvent.find { it.id == eventId } ?: EventModel(workspaceId = workspaceId),
|
||||
states.eventState.allEventInstances.find { it.eventId == eventId && it.instanceDate == instanceDate } == null,
|
||||
instanceDate,
|
||||
states.settings,
|
||||
viewModels.eventViewModel::onEvent
|
||||
viewModels.eventViewModel::onEvent,
|
||||
viewModels.workspaceViewModel::onEvent
|
||||
)
|
||||
},
|
||||
NavRoutes.NewEvent.route + "/{workspaceId}" + "/{eventId}" + "/{eventDate}" to { navController, states, viewModels, eventId, workspaceId, _, eventDate ->
|
||||
|
||||
@@ -33,6 +33,7 @@ class BackupWorker(
|
||||
val notesDao = DataModule.provideNotesDao(fluxDatabase)
|
||||
val workspaceDao = DataModule.provideWorkspaceDao(fluxDatabase)
|
||||
val todoDao = DataModule.provideTodoDao(fluxDatabase)
|
||||
val todoInstanceDao = DataModule.provideTodoInstanceDao(fluxDatabase)
|
||||
val habitDao = DataModule.provideHabitDao(fluxDatabase)
|
||||
val habitInstanceDao = DataModule.provideHabitInstanceDao(fluxDatabase)
|
||||
val journalDao = DataModule.provideJournalDao(fluxDatabase)
|
||||
@@ -52,6 +53,7 @@ class BackupWorker(
|
||||
workspaces = workspaceDao.getAll(),
|
||||
notes = notesDao.loadAllNotes(),
|
||||
todos = todoDao.loadAllLists(),
|
||||
todoInstances = todoInstanceDao.loadAllInstances(),
|
||||
habits = habitDao.loadAllHabits(),
|
||||
habitInstances = habitInstanceDao.loadAllInstances(),
|
||||
journals = journalDao.loadAllEntries(),
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.flux.other
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.ScheduleRequest
|
||||
import com.flux.data.model.isLive
|
||||
import com.flux.data.model.toScheduleRequest
|
||||
import com.flux.data.repository.EventRepository
|
||||
import com.flux.data.repository.HabitRepository
|
||||
import com.flux.data.repository.TodoRepository
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@@ -53,11 +55,13 @@ class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
val habitRepo = entryPoint.habitRepository()
|
||||
val eventRepo = entryPoint.eventRepository()
|
||||
val todoRepo = entryPoint.todoRepository()
|
||||
|
||||
val habits = habitRepo.loadAllHabits().filter { it.isLive() }.map { it.toScheduleRequest() }
|
||||
val events = eventRepo.loadAllEvents().filter { it.isLive() }.map { it.toScheduleRequest() }
|
||||
val todos = todoRepo.loadAllLists().filter { it.recurrence is RecurrenceRule.Weekly }.map { it.toScheduleRequest() }
|
||||
|
||||
return habits + events
|
||||
return habits + events + todos
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +70,5 @@ class BootReceiver : BroadcastReceiver() {
|
||||
interface ReceiverEntryPoint {
|
||||
fun habitRepository(): HabitRepository
|
||||
fun eventRepository(): EventRepository
|
||||
fun todoRepository(): TodoRepository
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ object Constants {
|
||||
}
|
||||
}
|
||||
|
||||
enum class ExportType {
|
||||
enum class ExportType {
|
||||
TXT,
|
||||
MARKDOWN,
|
||||
HTML,
|
||||
@@ -75,6 +75,18 @@ enum class ExportType {
|
||||
PDF
|
||||
}
|
||||
|
||||
enum class ConvertType {
|
||||
NOTE,
|
||||
TODO,
|
||||
JOURNAL,
|
||||
EVENT
|
||||
}
|
||||
|
||||
enum class DataCopyType {
|
||||
COPY,
|
||||
MOVE
|
||||
}
|
||||
|
||||
object Properties {
|
||||
// Regex pattern to extract the YAML block between "---" markers at the beginning of a note
|
||||
private val YAML_BLOCK_PATTERN =
|
||||
@@ -239,14 +251,16 @@ data class StyleRanges(
|
||||
val markerRanges: List<IntRange>,
|
||||
val linkRanges: List<IntRange>,
|
||||
val fencedCodeBlockInfoRanges: List<IntRange>,
|
||||
val codeBlockContentRanges: List<IntRange>
|
||||
val codeBlockContentRanges: List<IntRange>,
|
||||
val fenceMarkerRanges: List<IntRange>
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = StyleRanges(
|
||||
emptyList(), emptyList(), emptyList(),
|
||||
emptyList(), emptyList(), emptyList(),
|
||||
emptyList(), emptyList(), emptyList(),
|
||||
emptyList(), emptyList(), emptyList()
|
||||
emptyList(), emptyList(), emptyList(),
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ fun getNextOccurrence(rule: RecurrenceRule, startDateTime: Long): Long? {
|
||||
is RecurrenceRule.Yearly -> {
|
||||
findNextYearlyOccurrence(startDateTime, now)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
package com.flux.other
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Audiotrack
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.VideoFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import com.flux.R
|
||||
|
||||
// MarkdownSegment.kt
|
||||
|
||||
sealed class MarkdownSegment {
|
||||
/** Regular markdown text (bold, italic, inline code, links, etc.) */
|
||||
data class Text(val content: String) : MarkdownSegment()
|
||||
|
||||
/** A fenced code block */
|
||||
data class FencedCode(val info: String, val content: String) : MarkdownSegment()
|
||||
|
||||
/**
|
||||
* All media references found in the note — rendered as chips at the bottom of the card.
|
||||
* Never emitted as an inline segment; extracted globally by [extractMedia].
|
||||
*/
|
||||
data class MediaGroup(
|
||||
val images: List<String>,
|
||||
val videos: List<String>,
|
||||
val audio: List<String>
|
||||
) {
|
||||
val isEmpty get() = images.isEmpty() && videos.isEmpty() && audio.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension sets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "heic")
|
||||
private val VIDEO_EXTENSIONS = setOf("mp4", "mkv", "mov", "avi", "webm", "3gp", "m4v")
|
||||
private val AUDIO_EXTENSIONS = setOf("mp3", "aac", "ogg", "wav", "flac", "m4a", "opus", "wma")
|
||||
|
||||
// Markdown image: 
|
||||
private val MD_IMAGE_REGEX = Regex("""!\[[^\]]*]\(([^)]+)\)""")
|
||||
|
||||
// Markdown link: [text](url) — classified by URL extension
|
||||
private val MD_LINK_REGEX = Regex("""\[[^\]]*]\(([^)]+)\)""")
|
||||
|
||||
// HTML video tag: <video src="..." or <video src='...'
|
||||
private val HTML_VIDEO_REGEX = Regex("""<video[^>]+src=["']([^"']+)["'][^>]*>""", RegexOption.IGNORE_CASE)
|
||||
|
||||
// HTML audio tag: <audio src="..." or <audio src='...'
|
||||
private val HTML_AUDIO_REGEX = Regex("""<audio[^>]+src=["']([^"']+)["'][^>]*>""", RegexOption.IGNORE_CASE)
|
||||
|
||||
// HTML img tag: <img src="..." or <img src='...'
|
||||
private val HTML_IMG_REGEX = Regex("""<img[^>]+src=["']([^"']+)["'][^>]*>""", RegexOption.IGNORE_CASE)
|
||||
|
||||
// Closing tags to strip alongside the opening tags
|
||||
private val HTML_VIDEO_CLOSE = Regex("""</video\s*>""", RegexOption.IGNORE_CASE)
|
||||
private val HTML_AUDIO_CLOSE = Regex("""</audio\s*>""", RegexOption.IGNORE_CASE)
|
||||
|
||||
// Bare https?:// URL not inside () — classified by extension
|
||||
private val BARE_URL_REGEX = Regex("""(?<!\()https?://\S+""")
|
||||
|
||||
private val FENCE_REGEX = Regex(
|
||||
"""^(`{3,}|~{3,})([\w\-+#.]*)[ \t]*\r?\n(.*?)\r?\n?\1[ \t]*$""",
|
||||
setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private fun String.urlExtension() =
|
||||
substringAfterLast('.', "").lowercase().substringBefore('?').substringBefore('#').trim()
|
||||
|
||||
private enum class MediaType { IMAGE, VIDEO, AUDIO }
|
||||
|
||||
private fun classifyUrl(url: String): MediaType? {
|
||||
val ext = url.trim().urlExtension()
|
||||
return when (ext) {
|
||||
in IMAGE_EXTENSIONS -> MediaType.IMAGE
|
||||
in VIDEO_EXTENSIONS -> MediaType.VIDEO
|
||||
in AUDIO_EXTENSIONS -> MediaType.AUDIO
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global media extraction (call this separately from segmentMarkdown)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Scans the entire raw [text] for every media reference — markdown, HTML tags,
|
||||
* and bare URLs — and returns a [MarkdownSegment.MediaGroup] plus a cleaned
|
||||
* copy of the text with all media syntax removed.
|
||||
*
|
||||
* This is intentionally separate from [segmentMarkdown] so the caller
|
||||
* (e.g. NotesPreviewCard) can render chips wherever it wants (e.g. card bottom)
|
||||
* rather than inline inside the text column.
|
||||
*/
|
||||
data class MediaExtractionResult(
|
||||
val cleanedText: String,
|
||||
val media: MarkdownSegment.MediaGroup
|
||||
)
|
||||
|
||||
fun extractMedia(text: String): MediaExtractionResult {
|
||||
val images = mutableListOf<String>()
|
||||
val videos = mutableListOf<String>()
|
||||
val audio = mutableListOf<String>()
|
||||
val stripRanges = mutableListOf<IntRange>()
|
||||
|
||||
fun addImage(url: String, range: IntRange) { images += url; stripRanges += range }
|
||||
fun addVideo(url: String, range: IntRange) { videos += url; stripRanges += range }
|
||||
fun addAudio(url: String, range: IntRange) { audio += url; stripRanges += range }
|
||||
|
||||
// 1. HTML <video src="..."></video>
|
||||
HTML_VIDEO_REGEX.findAll(text).forEach { m ->
|
||||
addVideo(m.groupValues[1].trim(), m.range)
|
||||
}
|
||||
// strip </video> closing tags
|
||||
HTML_VIDEO_CLOSE.findAll(text).forEach { m -> stripRanges += m.range }
|
||||
|
||||
// 2. HTML <audio src="..."></audio>
|
||||
HTML_AUDIO_REGEX.findAll(text).forEach { m ->
|
||||
addAudio(m.groupValues[1].trim(), m.range)
|
||||
}
|
||||
HTML_AUDIO_CLOSE.findAll(text).forEach { m -> stripRanges += m.range }
|
||||
|
||||
// 3. HTML <img src="...">
|
||||
HTML_IMG_REGEX.findAll(text).forEach { m ->
|
||||
addImage(m.groupValues[1].trim(), m.range)
|
||||
}
|
||||
|
||||
// 4. Markdown images  — always image
|
||||
MD_IMAGE_REGEX.findAll(text).forEach { m ->
|
||||
if (stripRanges.any { it.contains(m.range.first) }) return@forEach
|
||||
addImage(m.groupValues[1].trim(), m.range)
|
||||
}
|
||||
|
||||
// 5. Markdown links [text](url) — classify by extension
|
||||
MD_LINK_REGEX.findAll(text).forEach { m ->
|
||||
if (stripRanges.any { it.contains(m.range.first) }) return@forEach
|
||||
val url = m.groupValues[1].trim()
|
||||
when (classifyUrl(url)) {
|
||||
MediaType.IMAGE -> addImage(url, m.range)
|
||||
MediaType.VIDEO -> addVideo(url, m.range)
|
||||
MediaType.AUDIO -> addAudio(url, m.range)
|
||||
null -> { /* plain link — leave it */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Bare URLs — classify by extension
|
||||
BARE_URL_REGEX.findAll(text).forEach { m ->
|
||||
if (stripRanges.any { it.contains(m.range.first) }) return@forEach
|
||||
val url = m.value.trim()
|
||||
when (classifyUrl(url)) {
|
||||
MediaType.IMAGE -> addImage(url, m.range)
|
||||
MediaType.VIDEO -> addVideo(url, m.range)
|
||||
MediaType.AUDIO -> addAudio(url, m.range)
|
||||
null -> { /* plain URL — leave it */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Build cleaned text by merging + removing strip ranges
|
||||
val merged = stripRanges.sortedBy { it.first }.fold(mutableListOf<IntRange>()) { acc, r ->
|
||||
if (acc.isEmpty() || acc.last().last < r.first - 1) acc.add(r)
|
||||
else acc[acc.lastIndex] = acc.last().first..maxOf(acc.last().last, r.last)
|
||||
acc
|
||||
}
|
||||
|
||||
var cleaned = text
|
||||
var offset = 0
|
||||
for (r in merged) {
|
||||
val start = (r.first - offset).coerceAtLeast(0)
|
||||
val end = (r.last - offset + 1).coerceAtMost(cleaned.length)
|
||||
if (start < end) {
|
||||
cleaned = cleaned.removeRange(start, end)
|
||||
offset += end - start
|
||||
}
|
||||
}
|
||||
|
||||
return MediaExtractionResult(
|
||||
cleanedText = cleaned.trim(),
|
||||
media = MarkdownSegment.MediaGroup(images, videos, audio)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Segment splitter (fenced code blocks only — media handled separately above)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fun segmentMarkdown(text: String): List<MarkdownSegment> {
|
||||
val segments = mutableListOf<MarkdownSegment>()
|
||||
var lastEnd = 0
|
||||
|
||||
for (match in FENCE_REGEX.findAll(text)) {
|
||||
if (match.range.first > lastEnd) {
|
||||
val before = text.substring(lastEnd, match.range.first)
|
||||
if (before.isNotBlank()) segments.add(MarkdownSegment.Text(before))
|
||||
}
|
||||
segments.add(MarkdownSegment.FencedCode(match.groupValues[2].trim(), match.groupValues[3]))
|
||||
lastEnd = match.range.last + 1
|
||||
}
|
||||
|
||||
if (lastEnd < text.length) {
|
||||
val remaining = text.substring(lastEnd)
|
||||
if (remaining.isNotBlank()) segments.add(MarkdownSegment.Text(remaining))
|
||||
}
|
||||
|
||||
if (segments.isEmpty()) segments.add(MarkdownSegment.Text(text))
|
||||
return segments
|
||||
}
|
||||
|
||||
fun openUrl(context: android.content.Context, url: String) {
|
||||
runCatching {
|
||||
val uri = url.toUri().let { parsed ->
|
||||
// Ensure the URL has a scheme so the browser can handle it
|
||||
if (parsed.scheme.isNullOrBlank()) "https://$url".toUri() else parsed
|
||||
}
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownBlock(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
linkColor: Color = Color(0xFF4A90D9),
|
||||
codeBlockBackground: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
codeBlockTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
onLinkClick: ((String) -> Unit)? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val handleLink: (String) -> Unit = onLinkClick ?: { url -> openUrl(context, url) }
|
||||
|
||||
// Strip media from text before segmenting
|
||||
val extracted = remember(text) { extractMedia(text) }
|
||||
val segments = remember(extracted.cleanedText) { segmentMarkdown(extracted.cleanedText) }
|
||||
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
segments.forEach { segment ->
|
||||
when (segment) {
|
||||
is MarkdownSegment.Text -> {
|
||||
val annotated = parseMarkdownContent(segment.content, linkColor)
|
||||
LinkAwareText(
|
||||
annotated = annotated,
|
||||
onLinkClick = handleLink,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
|
||||
is MarkdownSegment.FencedCode -> {
|
||||
FencedCodeBlock(
|
||||
info = segment.info,
|
||||
content = segment.content,
|
||||
background = codeBlockBackground,
|
||||
textColor = codeBlockTextColor,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MediaChipsRow(
|
||||
media: MarkdownSegment.MediaGroup,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {}
|
||||
) {
|
||||
if (media.isEmpty) return
|
||||
|
||||
FlowRow (
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = { onClick() },
|
||||
onLongPress = { onLongClick() }
|
||||
)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (media.images.isNotEmpty()) {
|
||||
MediaChip(icon = Icons.Default.Image, label = stringResource(R.string.image), count = media.images.size)
|
||||
}
|
||||
if (media.videos.isNotEmpty()) {
|
||||
MediaChip(icon = Icons.Default.VideoFile, label = stringResource(R.string.video), count = media.videos.size)
|
||||
}
|
||||
if (media.audio.isNotEmpty()) {
|
||||
MediaChip(icon = Icons.Default.Audiotrack, label = stringResource(R.string.audio), count = media.audio.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaChip(icon: ImageVector, label: String, count: Int) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Text(
|
||||
text = if (count > 1) "$label × $count" else label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkAwareText(
|
||||
annotated: AnnotatedString,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
|
||||
Text(
|
||||
text = annotated,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
onTextLayout = { textLayoutResult = it },
|
||||
modifier = Modifier.pointerInput(annotated) {
|
||||
detectTapGestures(
|
||||
onTap = { offset ->
|
||||
val layout = textLayoutResult ?: run { onClick(); return@detectTapGestures }
|
||||
val charOffset = layout.getOffsetForPosition(offset)
|
||||
val link = annotated
|
||||
.getStringAnnotations(URL_ANNOTATION_TAG, charOffset, charOffset)
|
||||
.firstOrNull()
|
||||
if (link != null) onLinkClick(link.item) else onClick()
|
||||
},
|
||||
onLongPress = { onLongClick() }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FencedCodeBlock(
|
||||
info: String,
|
||||
content: String,
|
||||
background: Color,
|
||||
textColor: Color,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = { onClick() },
|
||||
onLongPress = { onLongClick() }
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = background,
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||
if (info.isNotBlank()) {
|
||||
Text(
|
||||
text = info,
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
color = textColor.copy(alpha = 0.55f)
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(bottom = 8.dp)
|
||||
.align(Alignment.End)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = content,
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
color = textColor,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -120,6 +119,7 @@ object NotificationDispatcher {
|
||||
ensureChannel(context)
|
||||
|
||||
val isHabit = request.itemType == ReminderType.HABIT
|
||||
val isEvent = request.itemType == ReminderType.EVENT
|
||||
|
||||
val doneIntent = Intent(context, ReminderReceiver::class.java).apply {
|
||||
action = ACTION_MARK_DONE
|
||||
@@ -142,16 +142,23 @@ object NotificationDispatcher {
|
||||
.setContentTitle(request.title)
|
||||
.setContentText(request.description)
|
||||
.setSmallIcon(
|
||||
if (isHabit) R.drawable.calendar_check
|
||||
else R.drawable.check_list
|
||||
if (isHabit) R.drawable.routine
|
||||
else if(isEvent) R.drawable.calendar_check
|
||||
else R.drawable.to_do_list
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
// persistent notification
|
||||
builder.setOngoing(true).setAutoCancel(false)
|
||||
if (request.itemType != ReminderType.TODO) {
|
||||
builder.addAction(
|
||||
R.drawable.check_list,
|
||||
"Done",
|
||||
donePendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
val notification = builder.addAction(R.drawable.check_list, "Done", donePendingIntent).build()
|
||||
|
||||
val notification = builder.build()
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify((request.itemId + request.itemType.name).hashCode(), notification)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,32 @@ package com.flux.other
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import android.graphics.Canvas
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.print.PrintAttributes
|
||||
import android.print.PrintManager
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.FrameLayout
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.flux.ui.viewModel.NotesViewModel
|
||||
@@ -38,7 +47,6 @@ import org.commonmark.ext.front.matter.YamlFrontMatterExtension
|
||||
import org.commonmark.ext.gfm.strikethrough.Strikethrough
|
||||
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
|
||||
import org.commonmark.ext.gfm.tables.TableRow
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension
|
||||
import org.commonmark.ext.ins.Ins
|
||||
import org.commonmark.ext.ins.InsExtension
|
||||
import org.commonmark.node.AbstractVisitor
|
||||
@@ -60,7 +68,12 @@ import org.commonmark.parser.IncludeSourceSpans
|
||||
import org.commonmark.parser.Parser
|
||||
import androidx.core.net.toUri
|
||||
import com.flux.data.model.EventModel
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.occursOn
|
||||
import com.flux.data.model.toHtml
|
||||
import com.flux.data.model.toMarkdown
|
||||
import com.flux.data.model.toText
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.temporal.ChronoUnit
|
||||
@@ -261,6 +274,203 @@ fun shareNote(
|
||||
}
|
||||
}
|
||||
|
||||
fun shareTodo(
|
||||
context: Context,
|
||||
exportType: ExportType,
|
||||
list: TodoModel,
|
||||
readWebView: WebView?
|
||||
) {
|
||||
when (exportType) {
|
||||
|
||||
ExportType.TXT -> {
|
||||
shareTodoText(
|
||||
context,
|
||||
list.toText(),
|
||||
"text/plain"
|
||||
)
|
||||
}
|
||||
|
||||
ExportType.MARKDOWN -> {
|
||||
shareTodoText(
|
||||
context,
|
||||
list.toMarkdown(),
|
||||
"text/markdown"
|
||||
)
|
||||
}
|
||||
|
||||
ExportType.HTML -> {
|
||||
shareTodoText(
|
||||
context,
|
||||
list.toHtml(),
|
||||
"text/html"
|
||||
)
|
||||
}
|
||||
|
||||
ExportType.IMAGE -> {
|
||||
val activity = context.findActivity()
|
||||
exportHtmlAsImage(activity, list.toHtml()) { uri ->
|
||||
shareImageUri(context, uri)
|
||||
}
|
||||
}
|
||||
|
||||
ExportType.PDF -> {
|
||||
readWebView?.let {
|
||||
createWebPrintJob(
|
||||
it,
|
||||
context as? Activity,
|
||||
list.title
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportHtmlAsImage(
|
||||
activity: Activity,
|
||||
html: String,
|
||||
widthDp: Int = 480,
|
||||
onResult: (Uri) -> Unit
|
||||
) {
|
||||
val tag = "ExportHtmlAsImage"
|
||||
|
||||
val density = activity.resources.displayMetrics.density
|
||||
val widthPx = (widthDp * density).toInt()
|
||||
|
||||
val rootView = activity.window.decorView as ViewGroup
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
val webView = WebView(activity).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
layoutParams = FrameLayout.LayoutParams(widthPx, FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||
translationX = 10000f
|
||||
}
|
||||
rootView.addView(webView)
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
|
||||
mainHandler.postDelayed({
|
||||
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
var children = document.body.children;
|
||||
if (children.length === 0) return '0';
|
||||
var last = children[children.length - 1];
|
||||
var rect = last.getBoundingClientRect();
|
||||
var bottomWithPadding = rect.bottom + 32;
|
||||
return (bottomWithPadding * $density).toString();
|
||||
})()
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
|
||||
val contentHeightPx = result
|
||||
?.trim()
|
||||
?.removeSurrounding("\"")
|
||||
?.toFloatOrNull()
|
||||
?.toInt()
|
||||
?.coerceAtLeast(100)
|
||||
?: 0
|
||||
|
||||
if (contentHeightPx <= 0) {
|
||||
rootView.removeView(view)
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
// Force WebView to exactly contentHeightPx
|
||||
view.layoutParams = FrameLayout.LayoutParams(widthPx, contentHeightPx)
|
||||
view.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(contentHeightPx, View.MeasureSpec.EXACTLY)
|
||||
)
|
||||
view.layout(0, 0, widthPx, contentHeightPx)
|
||||
|
||||
view.post {
|
||||
try {
|
||||
val bitmap = createBitmap(widthPx, contentHeightPx)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.clipRect(0, 0, widthPx, contentHeightPx)
|
||||
view.draw(canvas)
|
||||
|
||||
rootView.removeView(view)
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val uri = saveBitmapAndGetUri(activity, bitmap)
|
||||
mainHandler.post { onResult(uri) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "❌ Failed to save bitmap: ${e.message}", e)
|
||||
}
|
||||
}.start()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "❌ Exception during bitmap drawing: ${e.message}", e)
|
||||
rootView.removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(tag, "contentHeightPx = $contentHeightPx, density = $density")
|
||||
}
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: android.webkit.WebResourceRequest?,
|
||||
error: android.webkit.WebResourceError?
|
||||
) {
|
||||
Log.e(tag, "❌ onReceivedError | url=${request?.url} | error=${error?.description}")
|
||||
}
|
||||
}
|
||||
|
||||
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)
|
||||
}
|
||||
|
||||
fun saveBitmapAndGetUri(context: Context, bitmap: Bitmap): Uri {
|
||||
val cacheDir = File(context.cacheDir, "shared_images").apply { mkdirs() }
|
||||
val file = File(cacheDir, "html_${System.currentTimeMillis()}.png")
|
||||
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||
}
|
||||
|
||||
fun shareImageUri(context: Context, uri: Uri) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "image/png"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, "Share image via"))
|
||||
}
|
||||
|
||||
fun Context.findActivity(): Activity {
|
||||
var ctx = this
|
||||
while (ctx is ContextWrapper) {
|
||||
if (ctx is Activity) return ctx
|
||||
ctx = ctx.baseContext
|
||||
}
|
||||
throw IllegalStateException("Activity not found")
|
||||
}
|
||||
|
||||
private fun shareTodoText(
|
||||
context: Context,
|
||||
text: String,
|
||||
mimeType: String
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = mimeType
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, null)
|
||||
)
|
||||
}
|
||||
|
||||
private fun shareAsText(
|
||||
context: Context,
|
||||
type: ExportType,
|
||||
@@ -434,7 +644,8 @@ val PARSER: Parser =
|
||||
|
||||
fun findTagRanges(text: String): StyleRanges {
|
||||
if (text.isEmpty()) return StyleRanges.EMPTY
|
||||
val document = PARSER.parse(text)
|
||||
val normalizedText = if (text.endsWith('\n')) text else "$text\n"
|
||||
val document = PARSER.parse(normalizedText)
|
||||
val codeRanges = mutableListOf<IntRange>()
|
||||
val boldRanges = mutableListOf<IntRange>()
|
||||
val italicRanges = mutableListOf<IntRange>()
|
||||
@@ -447,6 +658,7 @@ fun findTagRanges(text: String): StyleRanges {
|
||||
val linkRanges = mutableListOf<IntRange>()
|
||||
val fencedCodeBlockInfoRanges = mutableListOf<IntRange>()
|
||||
val codeBlockContentRanges = mutableListOf<IntRange>()
|
||||
val fenceMarkerRanges = mutableListOf<IntRange>() // NEW: tracks ``` open/close lines separately
|
||||
|
||||
|
||||
document.accept(object : AbstractVisitor() {
|
||||
@@ -551,8 +763,6 @@ fun findTagRanges(text: String): StyleRanges {
|
||||
override fun visit(link: Link) {
|
||||
val span = link.sourceSpans.firstOrNull()
|
||||
if (span != null) {
|
||||
// The entire link including text and URL needs to be styled
|
||||
// Format is [text](url)
|
||||
linkRanges.add(span.inputIndex until (span.inputIndex + span.length))
|
||||
}
|
||||
visitChildren(link)
|
||||
@@ -621,24 +831,47 @@ fun findTagRanges(text: String): StyleRanges {
|
||||
}
|
||||
|
||||
override fun visit(fencedCodeBlock: FencedCodeBlock) {
|
||||
val span = fencedCodeBlock.sourceSpans.firstOrNull() ?: return
|
||||
val spans = fencedCodeBlock.sourceSpans
|
||||
if (spans.isEmpty()) return
|
||||
|
||||
// Get the opening fence marker (```language)
|
||||
val openingFenceStartIndex = span.inputIndex
|
||||
val openingMarkerLength = fencedCodeBlock.openingFenceLength ?: return
|
||||
val infoStringStartIndex = openingFenceStartIndex + openingMarkerLength
|
||||
markerRanges.add(openingFenceStartIndex until infoStringStartIndex) // ```
|
||||
val infoStringLength = fencedCodeBlock.info?.length ?: 0
|
||||
fencedCodeBlockInfoRanges.add(infoStringStartIndex until (infoStringStartIndex + infoStringLength)) // language
|
||||
spans.forEachIndexed { index, span ->
|
||||
when (index) {
|
||||
0 -> {
|
||||
// opening ``` or ```kotlin — goes into fenceMarkerRanges, not markerRanges
|
||||
fenceMarkerRanges.add(
|
||||
span.inputIndex until
|
||||
(span.inputIndex + span.length)
|
||||
)
|
||||
|
||||
val closingMarkerLength = fencedCodeBlock.closingFenceLength ?: return
|
||||
val blockContentLength =
|
||||
if (fencedCodeBlock.literal.isEmpty()) 0 else fencedCodeBlock.literal.length + 1
|
||||
val fence =
|
||||
openingFenceStartIndex + openingMarkerLength + infoStringLength + blockContentLength
|
||||
codeBlockContentRanges.add((openingFenceStartIndex + openingMarkerLength + infoStringLength) until fence) // content
|
||||
if (fence + closingMarkerLength <= text.length) {
|
||||
markerRanges.add(fence until (fence + closingMarkerLength))
|
||||
val openingText = text.substring(
|
||||
span.inputIndex,
|
||||
(span.inputIndex + span.length).coerceAtMost(text.length)
|
||||
)
|
||||
|
||||
val firstSpace = openingText.indexOf(' ')
|
||||
if (firstSpace != -1 && firstSpace + 1 < openingText.length) {
|
||||
fencedCodeBlockInfoRanges.add(
|
||||
(span.inputIndex + firstSpace + 1) until
|
||||
(span.inputIndex + openingText.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
spans.lastIndex -> {
|
||||
// closing ``` — goes into fenceMarkerRanges, not markerRanges
|
||||
fenceMarkerRanges.add(
|
||||
span.inputIndex until
|
||||
(span.inputIndex + span.length)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
codeBlockContentRanges.add(
|
||||
span.inputIndex until
|
||||
(span.inputIndex + span.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,99 +896,315 @@ fun findTagRanges(text: String): StyleRanges {
|
||||
markerRanges = markerRanges,
|
||||
linkRanges = linkRanges,
|
||||
fencedCodeBlockInfoRanges = fencedCodeBlockInfoRanges,
|
||||
codeBlockContentRanges = codeBlockContentRanges
|
||||
codeBlockContentRanges = codeBlockContentRanges,
|
||||
fenceMarkerRanges = fenceMarkerRanges
|
||||
)
|
||||
}
|
||||
|
||||
fun parseMarkdownContent(text: String): AnnotatedString {
|
||||
private data class LinkInfo(
|
||||
val origStart: Int, // start in the pre-transform text
|
||||
val origEnd: Int, // end in the pre-transform text
|
||||
val display: String, // visible label ([display](url))
|
||||
val url: String // destination URL
|
||||
)
|
||||
|
||||
private fun collectLinks(text: String): List<LinkInfo> {
|
||||
val document = PARSER.parse(text)
|
||||
val links = mutableListOf<LinkInfo>()
|
||||
|
||||
document.accept(object : AbstractVisitor() {
|
||||
override fun visit(link: Link) {
|
||||
val span = link.sourceSpans.firstOrNull() ?: return
|
||||
val display = buildString {
|
||||
var child = link.firstChild
|
||||
while (child != null) {
|
||||
if (child is Text) append(child.literal)
|
||||
child = child.next
|
||||
}
|
||||
}.ifEmpty { link.destination }
|
||||
links.add(LinkInfo(span.inputIndex, span.inputIndex + span.length, display, link.destination))
|
||||
}
|
||||
|
||||
override fun visit(image: Image) {
|
||||
val span = image.sourceSpans.firstOrNull() ?: return
|
||||
val altText = buildString {
|
||||
var child = image.firstChild
|
||||
while (child != null) {
|
||||
if (child is Text) append(child.literal)
|
||||
child = child.next
|
||||
}
|
||||
}.ifEmpty { image.destination }
|
||||
links.add(LinkInfo(span.inputIndex, span.inputIndex + span.length, altText, image.destination))
|
||||
}
|
||||
})
|
||||
|
||||
return links.sortedBy { it.origStart }
|
||||
}
|
||||
|
||||
private fun transformLinks(rawText: String): Pair<String, List<LinkInfo>> {
|
||||
val links = collectLinks(rawText)
|
||||
if (links.isEmpty()) return rawText to emptyList()
|
||||
|
||||
val sb = StringBuilder()
|
||||
val adjustedLinks = mutableListOf<LinkInfo>()
|
||||
var cursor = 0 // current position in rawText
|
||||
var shift = 0 // cumulative length change
|
||||
|
||||
for (link in links) {
|
||||
// Append everything before this link unchanged
|
||||
sb.append(rawText, cursor, link.origStart)
|
||||
|
||||
// Append only the display text
|
||||
val newStart = link.origStart + shift
|
||||
sb.append(link.display)
|
||||
val newEnd = newStart + link.display.length
|
||||
|
||||
adjustedLinks.add(link.copy(origStart = newStart, origEnd = newEnd))
|
||||
|
||||
shift += link.display.length - (link.origEnd - link.origStart)
|
||||
cursor = link.origEnd
|
||||
}
|
||||
|
||||
// Append any remaining text after the last link
|
||||
sb.append(rawText, cursor, rawText.length)
|
||||
|
||||
return sb.toString() to adjustedLinks
|
||||
}
|
||||
|
||||
// URL annotation tag used for click handling
|
||||
const val URL_ANNOTATION_TAG = "URL"
|
||||
|
||||
fun parseMarkdownContent(text: String, linkColor: Color = Color.Blue): AnnotatedString {
|
||||
if (text.isBlank()) return AnnotatedString(text)
|
||||
val textWithoutProperties =
|
||||
text.splitPropertiesAndContent().second
|
||||
.replace("- [ ]", "☐")
|
||||
.replace("- [x]", "☑")
|
||||
val styleRanges = findTagRanges(textWithoutProperties)
|
||||
|
||||
val rawText = text.splitPropertiesAndContent().second
|
||||
.replace("- [ ]", "☐")
|
||||
.replace("- [x]", "☑")
|
||||
.replace("- [X]", "☑")
|
||||
.replace("-", "•")
|
||||
|
||||
// Transform [display](url) → display, and get adjusted link positions
|
||||
val (transformedText, adjustedLinks) = transformLinks(rawText)
|
||||
|
||||
val styleRanges = findTagRanges(transformedText)
|
||||
|
||||
return buildAnnotatedString {
|
||||
fun safeAddStyle(style: SpanStyle, start: Int, end: Int) {
|
||||
val safeStart = start.coerceAtLeast(0).coerceAtMost(text.length)
|
||||
val safeEnd = end.coerceAtLeast(0).coerceAtMost(text.length)
|
||||
addStyle(style, safeStart, safeEnd)
|
||||
val safeStart = start.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
val safeEnd = end.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
if (safeStart < safeEnd) addStyle(style, safeStart, safeEnd)
|
||||
}
|
||||
|
||||
fun safeAddStyle(style: ParagraphStyle, start: Int, end: Int) {
|
||||
val safeStart = start.coerceAtLeast(0).coerceAtMost(text.length)
|
||||
val safeEnd = end.coerceAtLeast(0).coerceAtMost(text.length)
|
||||
addStyle(style, safeStart, safeEnd)
|
||||
val safeStart = start.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
val safeEnd = end.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
if (safeStart < safeEnd) addStyle(style, safeStart, safeEnd)
|
||||
}
|
||||
|
||||
styleRanges.apply {
|
||||
// Inline code: `code`
|
||||
codeRanges.forEach { range ->
|
||||
safeAddStyle(CODE_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 1 + 1, range.last + 1)
|
||||
safeAddStyle(
|
||||
CODE_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
|
||||
var delimiterLength = 0
|
||||
var i = range.first
|
||||
|
||||
while (
|
||||
i <= range.last &&
|
||||
transformedText.getOrNull(i) == '`'
|
||||
) {
|
||||
delimiterLength++
|
||||
i++
|
||||
}
|
||||
|
||||
if (delimiterLength > 0) {
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.first,
|
||||
range.first + delimiterLength
|
||||
)
|
||||
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.last - delimiterLength + 1,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fenced code block content lines
|
||||
codeBlockContentRanges.forEach { range ->
|
||||
safeAddStyle(
|
||||
CODE_BLOCK_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
// Fenced code block opening/closing ``` lines:
|
||||
// Apply CODE_BLOCK_STYLE so the background is contiguous with the content,
|
||||
// then apply SYMBOL_STYLE on top to dim/style the fence markers themselves.
|
||||
fenceMarkerRanges.forEach { range ->
|
||||
safeAddStyle(
|
||||
CODE_BLOCK_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
boldItalicRanges.forEach { range ->
|
||||
safeAddStyle(BOLD_ITALIC_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 3)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 3 + 1, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 2, range.last + 1)
|
||||
}
|
||||
|
||||
boldRanges.forEach { range ->
|
||||
safeAddStyle(BOLD_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 2)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 2 + 1, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 1, range.last + 1)
|
||||
}
|
||||
|
||||
italicRanges.forEach { range ->
|
||||
safeAddStyle(ITALIC_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 1 + 1, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last, range.last + 1)
|
||||
}
|
||||
|
||||
highlightRanges.forEach { range ->
|
||||
safeAddStyle(HIGHLIGHT_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 2)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 2 + 1, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 1, range.last + 1)
|
||||
}
|
||||
|
||||
val combinedRanges = (strikethroughRanges + underlineRanges).distinct()
|
||||
val combinedRanges =
|
||||
(strikethroughRanges + underlineRanges).distinct()
|
||||
|
||||
combinedRanges.forEach { range ->
|
||||
val hasStrikethrough = strikethroughRanges.any { it.overlaps(range) }
|
||||
val hasUnderline = underlineRanges.any { it.overlaps(range) }
|
||||
val hasStrikethrough =
|
||||
strikethroughRanges.any { it.overlaps(range) }
|
||||
|
||||
val hasUnderline =
|
||||
underlineRanges.any { it.overlaps(range) }
|
||||
|
||||
val style = when {
|
||||
hasStrikethrough && hasUnderline -> STRIKETHROUGH_AND_UNDERLINE_STYLE
|
||||
hasStrikethrough -> STRIKETHROUGH_STYLE
|
||||
hasUnderline -> UNDERLINE_STYLE
|
||||
hasStrikethrough && hasUnderline ->
|
||||
STRIKETHROUGH_AND_UNDERLINE_STYLE
|
||||
|
||||
hasStrikethrough ->
|
||||
STRIKETHROUGH_STYLE
|
||||
|
||||
hasUnderline ->
|
||||
UNDERLINE_STYLE
|
||||
|
||||
else -> return@forEach
|
||||
}
|
||||
safeAddStyle(style, range.first, range.last + 1)
|
||||
|
||||
safeAddStyle(
|
||||
style,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
strikethroughRanges.forEach { range ->
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 2)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 2 + 1, range.last + 1)
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.first,
|
||||
range.first + 2
|
||||
)
|
||||
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.last - 1,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
underlineRanges.forEach { range ->
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + 2)
|
||||
safeAddStyle(SYMBOL_STYLE, range.last - 2 + 1, range.last + 1)
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.first,
|
||||
range.first + 2
|
||||
)
|
||||
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.last - 1,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
headerRanges.forEach { (range, level) ->
|
||||
safeAddStyle(HEADER_STYLES[level - 1], range.first, range.last + 1)
|
||||
safeAddStyle(HEADER_LINE_STYLES[level - 1], range.first, range.last + 1)
|
||||
safeAddStyle(SYMBOL_STYLE, range.first, range.first + level + 1)
|
||||
safeAddStyle(
|
||||
HEADER_STYLES[level - 1],
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
|
||||
safeAddStyle(
|
||||
HEADER_LINE_STYLES[level - 1],
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
|
||||
safeAddStyle(
|
||||
SYMBOL_STYLE,
|
||||
range.first,
|
||||
range.first + level + 1
|
||||
)
|
||||
}
|
||||
|
||||
// Add styling for list markers
|
||||
markerRanges.forEach { range ->
|
||||
safeAddStyle(MARKER_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(
|
||||
MARKER_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
fencedCodeBlockInfoRanges.forEach { range ->
|
||||
safeAddStyle(KEYWORD_STYLE, range.first, range.last + 1)
|
||||
safeAddStyle(
|
||||
KEYWORD_STYLE,
|
||||
range.first,
|
||||
range.last + 1
|
||||
)
|
||||
}
|
||||
codeBlockContentRanges.forEach { range ->
|
||||
safeAddStyle(CODE_BLOCK_STYLE, range.first, range.last + 1)
|
||||
|
||||
}
|
||||
|
||||
// Apply link styles and URL annotations using the adjusted (post-transform) positions
|
||||
for (link in adjustedLinks) {
|
||||
val start = link.origStart.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
val end = link.origEnd.coerceAtLeast(0).coerceAtMost(transformedText.length)
|
||||
if (start < end) {
|
||||
addStyle(
|
||||
SpanStyle(
|
||||
color = linkColor,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
// Attach the URL so ClickableText callers can open it
|
||||
addStringAnnotation(
|
||||
tag = URL_ANNOTATION_TAG,
|
||||
annotation = link.url,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
}
|
||||
}
|
||||
append(textWithoutProperties)
|
||||
|
||||
append(transformedText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ fun NoteDetailsTopBar(
|
||||
onShareNote: () -> Unit,
|
||||
onSaveNote: () -> Unit,
|
||||
onPrintNote: () -> Unit,
|
||||
onConvertNote: () ->Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
@@ -132,7 +135,7 @@ fun NoteDetailsTopBar(
|
||||
actions = {
|
||||
if(!isReadView){ IconButton({onSearchClick()}) { Icon(if(isSearching) Icons.Default.SearchOff else Icons.Default.Search, null) } }
|
||||
IconButton({onOutlineClicked()}) { Icon(Icons.Default.Summarize, null) }
|
||||
DropdownMenuWithDetails(isPinned, onTogglePinned, onAddLabel, onAboutClicked, onShareNote, onSaveNote, onPrintNote, onDelete) }
|
||||
DropdownMenuWithDetails(isPinned, onTogglePinned, onAddLabel, onAboutClicked, onShareNote, onSaveNote, onPrintNote, onConvertNote, onCopyNote, onCloneNote, onDelete) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -152,6 +155,9 @@ fun JournalDetailsTopBar(
|
||||
onShareNote: () -> Unit,
|
||||
onSaveNote: () -> Unit,
|
||||
onPrintNote: () -> Unit,
|
||||
onConvertNote: () ->Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
@@ -190,7 +196,7 @@ fun JournalDetailsTopBar(
|
||||
actions = {
|
||||
if(!isReadView){ IconButton({onSearchClick()}) { Icon(if(isSearching) Icons.Default.SearchOff else Icons.Default.Search, null) } }
|
||||
IconButton({onOutlineClicked()}) { Icon(Icons.Default.Summarize, null) }
|
||||
JournalDropdownMenu(onAddLabel, onAboutClicked, onShareNote, onSaveNote, onPrintNote, onDelete)
|
||||
JournalDropdownMenu(onAddLabel, onAboutClicked, onShareNote, onSaveNote, onPrintNote, onDelete, onConvertNote, onCopyNote, onCloneNote)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -357,9 +363,9 @@ fun SpaceTopBar(
|
||||
val titleSectionHeightDp = 12.dp + titleRowDp + if (hasDescription) 2.dp + bodyLineHeightDp else 0.dp
|
||||
|
||||
val expandedHeightDp = if (hasCover)
|
||||
coverHeightDp + titleSectionHeightDp + statusBarHeight - 32.dp
|
||||
coverHeightDp + titleSectionHeightDp + statusBarHeight - 24.dp
|
||||
else
|
||||
toolbarHeight + titleSectionHeightDp + 24.dp
|
||||
toolbarHeight + titleSectionHeightDp + 28.dp
|
||||
|
||||
val expandedPx = with(density) { expandedHeightDp.toPx() }
|
||||
val collapsedPx = with(density) { toolbarHeight.toPx() }
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flux.R
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
@@ -106,6 +107,7 @@ fun RecurrenceRule.label(): String = when (this) {
|
||||
is RecurrenceRule.Weekly -> stringResource(R.string.Weekly)
|
||||
is RecurrenceRule.Monthly -> stringResource(R.string.Monthly)
|
||||
is RecurrenceRule.Yearly -> stringResource(R.string.Yearly)
|
||||
else -> ""
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -219,33 +221,44 @@ fun RecurrenceBottomSheet(
|
||||
stringResource(R.string.saturday_short),
|
||||
stringResource(R.string.sunday_short)
|
||||
)
|
||||
Row (
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
maxItemsInEachRow = 7
|
||||
) {
|
||||
weekdays.forEachIndexed { index, day ->
|
||||
val isSelected = index in rule.daysOfWeek
|
||||
|
||||
Card(
|
||||
onClick = {
|
||||
val newDays = rule.daysOfWeek.toMutableList()
|
||||
if (index in newDays) newDays.remove(index) else newDays.add(index)
|
||||
tempRule = rule.copy(daysOfWeek = newDays)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 2.dp),
|
||||
onClick = {},
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp),
|
||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
|
||||
containerColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp),
|
||||
|
||||
contentColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = day,
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -277,6 +290,8 @@ fun RecurrenceBottomSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,5 +349,7 @@ fun RecurrenceBottomSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
} as Long
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.flux.ui.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
@@ -9,9 +10,12 @@ 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.heightIn
|
||||
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.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccessTime
|
||||
@@ -21,12 +25,16 @@ import androidx.compose.material.icons.filled.FontDownload
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -36,25 +44,34 @@ import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.material3.rememberTimePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.flux.R
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.other.icons
|
||||
import com.flux.ui.screens.settings.CircleWrapper
|
||||
import com.flux.ui.theme.FONTS
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun convertMillisToDate(millis: Long): String {
|
||||
val formatter = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
formatter.timeZone = TimeZone.getDefault()
|
||||
return formatter.format(Date(millis))
|
||||
}
|
||||
|
||||
@@ -70,7 +87,16 @@ fun DatePickerModal(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDateSelected(datePickerState.selectedDateMillis)
|
||||
val normalized = datePickerState.selectedDateMillis?.let { millis ->
|
||||
Calendar.getInstance().apply {
|
||||
timeInMillis = millis
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
onDateSelected(normalized)
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.Set))
|
||||
@@ -258,4 +284,123 @@ fun FontDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DataCopyDialog(
|
||||
workspaces: List<WorkspaceModel>,
|
||||
onConfirm: (DataCopyType, List<WorkspaceModel>) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
){
|
||||
val selectedWorkspaces = remember { mutableStateListOf<WorkspaceModel>() }
|
||||
var selectedType by remember { mutableStateOf(DataCopyType.COPY) }
|
||||
|
||||
AlertDialog(
|
||||
icon = {
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 0,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
selectedType = DataCopyType.COPY
|
||||
selectedWorkspaces.clear()
|
||||
},
|
||||
selected = selectedType == DataCopyType.COPY,
|
||||
label = { Text("Copy") }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 1,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
selectedType = DataCopyType.MOVE
|
||||
selectedWorkspaces.clear()
|
||||
},
|
||||
selected = selectedType == DataCopyType.MOVE,
|
||||
label = { Text("Move") }
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("Select Workspaces")
|
||||
},
|
||||
text = {
|
||||
LazyColumn (Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp)) {
|
||||
items(workspaces) { workspace->
|
||||
val isChecked = selectedWorkspaces.contains(workspace)
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(50))
|
||||
.padding(vertical = 2.dp)
|
||||
.clickable {
|
||||
if (isChecked) {
|
||||
selectedWorkspaces.remove(workspace)
|
||||
} else {
|
||||
if(selectedType== DataCopyType.MOVE) if(selectedWorkspaces.isNotEmpty()) selectedWorkspaces.clear()
|
||||
selectedWorkspaces.add(workspace)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(50)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(vertical = 6.dp, horizontal = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
CircleWrapper(color = MaterialTheme.colorScheme.primary) {
|
||||
Icon(
|
||||
icons[workspace.icon],
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = workspace.title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.width(150.dp)
|
||||
)
|
||||
}
|
||||
Checkbox(checked = isChecked, onCheckedChange = {
|
||||
if (isChecked) {
|
||||
selectedWorkspaces.remove(workspace)
|
||||
} else {
|
||||
if(selectedType== DataCopyType.MOVE) if(selectedWorkspaces.isNotEmpty()) selectedWorkspaces.clear()
|
||||
selectedWorkspaces.add(workspace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(selectedType, selectedWorkspaces.toList())
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Dismiss")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.flux.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Notes
|
||||
@@ -11,6 +14,8 @@ import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.RemoveCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Analytics
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.ControlPointDuplicate
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
@@ -23,6 +28,7 @@ import androidx.compose.material.icons.outlined.PhotoSizeSelectActual
|
||||
import androidx.compose.material.icons.outlined.Print
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material.icons.outlined.SwapHoriz
|
||||
import androidx.compose.material.icons.outlined.TaskAlt
|
||||
import androidx.compose.material.icons.outlined.TrackChanges
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -40,7 +46,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flux.R
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
@@ -54,6 +62,9 @@ fun DropdownMenuWithDetails(
|
||||
onShareNote: () -> Unit,
|
||||
onSaveNote: () -> Unit,
|
||||
onPrintNote: () -> Unit,
|
||||
onConvertNote: () ->Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
@@ -95,6 +106,33 @@ fun DropdownMenuWithDetails(
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clone") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ControlPointDuplicate, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCloneNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCopyNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Convert") },
|
||||
leadingIcon = { Icon(Icons.Outlined.SwapHoriz, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onConvertNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.share)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Share, contentDescription = null) },
|
||||
@@ -154,7 +192,10 @@ fun JournalDropdownMenu(
|
||||
onShareNote: () -> Unit,
|
||||
onSaveNote: () -> Unit,
|
||||
onPrintNote: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
onDelete: () -> Unit,
|
||||
onConvertNote: () ->Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -185,6 +226,33 @@ fun JournalDropdownMenu(
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clone") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ControlPointDuplicate, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCloneNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCopyNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Convert") },
|
||||
leadingIcon = { Icon(Icons.Outlined.SwapHoriz, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onConvertNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.share)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Share, contentDescription = null) },
|
||||
@@ -245,81 +313,92 @@ fun SpacesMenu(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val selectedSpaces = workspace.selectedSpaces
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
DropdownMenu(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismiss
|
||||
onDismissRequest = onDismiss,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
scrollState = scrollState,
|
||||
modifier = Modifier.heightIn(max = 300.dp)
|
||||
) {
|
||||
if (selectedSpaces.contains(1)) {
|
||||
|
||||
@Composable
|
||||
fun MenuItem(
|
||||
visible: Boolean,
|
||||
id: Int,
|
||||
title: String,
|
||||
icon: ImageVector
|
||||
) {
|
||||
if (!visible) return
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Notes)) },
|
||||
leadingIcon = { Icon(Icons.AutoMirrored.Default.Notes, contentDescription = null) },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.widthIn(max = 100.dp)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(icon, null)
|
||||
},
|
||||
onClick = {
|
||||
onConfirm(1)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(2)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.To_Do)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.TaskAlt, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(2)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(3)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Events)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Event, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(3)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(4)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Journal)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.AutoStories, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(4)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(5)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Habits)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.EventAvailable, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(5)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(7)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.progress_tracker)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.TrackChanges, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(7)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(6)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Analytics)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Analytics, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(6)
|
||||
onConfirm(id)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(1),
|
||||
1,
|
||||
stringResource(R.string.Notes),
|
||||
Icons.AutoMirrored.Default.Notes
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(2),
|
||||
2,
|
||||
stringResource(R.string.To_Do),
|
||||
Icons.Outlined.TaskAlt
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(3),
|
||||
3,
|
||||
stringResource(R.string.Events),
|
||||
Icons.Outlined.Event
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(4),
|
||||
4,
|
||||
stringResource(R.string.Journal),
|
||||
Icons.Outlined.AutoStories
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(5),
|
||||
5,
|
||||
stringResource(R.string.Habits),
|
||||
Icons.Outlined.EventAvailable
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(7),
|
||||
7,
|
||||
stringResource(R.string.progress_tracker),
|
||||
Icons.Outlined.TrackChanges
|
||||
)
|
||||
|
||||
MenuItem(
|
||||
selectedSpaces.contains(6),
|
||||
6,
|
||||
stringResource(R.string.Analytics),
|
||||
Icons.Outlined.Analytics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,4 +499,196 @@ fun WorkspaceMore(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TodoDropdownMenu(
|
||||
canShare: Boolean,
|
||||
onShare: () -> Unit,
|
||||
onPrint: () -> Unit,
|
||||
onClone: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
onConvert: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier) {
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "More options"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
if(canShare){
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.share)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Share, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onShare()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.print)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Print, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onPrint()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clone") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ControlPointDuplicate, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onClone()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCopy()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Convert") },
|
||||
leadingIcon = { Icon(Icons.Outlined.SwapHoriz, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onConvert()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = MaterialTheme.colorScheme.error,
|
||||
leadingIconColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
text = { Text(stringResource(R.string.delete)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Delete, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onDelete()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventDropdownMenu(
|
||||
onDelete: () -> Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier) {
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "More options"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clone") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ControlPointDuplicate, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCloneNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCopyNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = MaterialTheme.colorScheme.error,
|
||||
leadingIconColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
text = { Text(stringResource(R.string.delete)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Delete, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onDelete()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HabitDropdownMenu(
|
||||
onDelete: () -> Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier) {
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "More options"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clone") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ControlPointDuplicate, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCloneNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
leadingIcon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onCopyNote()
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = MaterialTheme.colorScheme.error,
|
||||
leadingIconColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
text = { Text(stringResource(R.string.delete)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Delete, contentDescription = null) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onDelete()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.flux.ui.common
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -46,6 +45,8 @@ fun HabitScaffold(
|
||||
onDeleteClicked: () -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
onEditClicked: () -> Unit,
|
||||
onCopyNote: () -> Unit,
|
||||
onCloneNote: () -> Unit,
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -64,13 +65,11 @@ fun HabitScaffold(
|
||||
},
|
||||
actions = {
|
||||
IconButton(onEditClicked) { Icon(Icons.Default.Edit, null) }
|
||||
IconButton(onDeleteClicked) {
|
||||
Icon(
|
||||
Icons.Default.DeleteOutline,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
HabitDropdownMenu(
|
||||
onDeleteClicked,
|
||||
onCopyNote,
|
||||
onCloneNote
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.flux.ui.events
|
||||
|
||||
import android.content.Context
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
|
||||
sealed class TodoEvents {
|
||||
data class DeleteAllWorkspaceLists(val workspaceId: String) : TodoEvents()
|
||||
data class DeleteList(val data: TodoModel) : TodoEvents()
|
||||
data class UpsertList(val data: TodoModel) : TodoEvents()
|
||||
data class DeleteAllWorkspaceLists(val context: Context, val workspaceId: String) : TodoEvents()
|
||||
data class DeleteList(val context: Context, val data: TodoModel) : TodoEvents()
|
||||
data class UpsertList(val context: Context, val isRemovingReminder: Boolean, val data: TodoModel) : TodoEvents()
|
||||
data class CreateInstance(val listId: String, val workspaceId: String) : TodoEvents()
|
||||
data class UpsertInstance(val instance: TodoInstance) : TodoEvents()
|
||||
}
|
||||
@@ -74,6 +74,7 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
@@ -314,7 +315,7 @@ fun EventCard(
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||
Text(title, style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
@@ -374,6 +375,8 @@ fun getRecurrenceText(context: Context, repeat: RecurrenceRule, startDateTime: L
|
||||
localDate.format(DateTimeFormatter.ofPattern("MMM dd"))
|
||||
)
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.flux.ui.screens.events
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -14,7 +15,6 @@ import androidx.compose.material.icons.filled.Pending
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.SportsScore
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.NotificationsActive
|
||||
import androidx.compose.material.icons.outlined.Pending
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -48,9 +48,14 @@ import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.EventModel
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.ui.common.DataCopyDialog
|
||||
import com.flux.ui.common.DeleteAlert
|
||||
import com.flux.ui.common.EventDropdownMenu
|
||||
import com.flux.ui.events.TaskEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
import com.flux.ui.state.Settings
|
||||
import java.time.ZoneId
|
||||
import java.time.Instant
|
||||
@@ -60,12 +65,14 @@ import java.time.format.DateTimeFormatter
|
||||
@Composable
|
||||
fun EventDetails(
|
||||
navController: NavController,
|
||||
workspaces: List<WorkspaceModel>,
|
||||
workspaceId: String,
|
||||
event: EventModel,
|
||||
isPending: Boolean,
|
||||
instanceDate: Long,
|
||||
settings: Settings,
|
||||
onTaskEvents: (TaskEvents) -> Unit
|
||||
onTaskEvents: (TaskEvents) -> Unit,
|
||||
onWorkspaceEvents: (WorkspaceEvents) -> Unit
|
||||
) {
|
||||
var title by remember { mutableStateOf(event.title) }
|
||||
var description by remember { mutableStateOf(event.description) }
|
||||
@@ -75,6 +82,10 @@ fun EventDetails(
|
||||
val context = LocalContext.current
|
||||
val time = event.startDateTime.toFormattedTime(settings.data.is24HourFormat)
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var showDataCopyDialog by remember { mutableStateOf(false) }
|
||||
val cloneString = stringResource(R.string.clone_created_successfully)
|
||||
val contentCopiedString = stringResource(R.string.content_copied)
|
||||
val contentMovedString = stringResource(R.string.content_moved)
|
||||
|
||||
if(showDeleteDialog){
|
||||
DeleteAlert({
|
||||
@@ -111,13 +122,17 @@ fun EventDetails(
|
||||
contentDescription = "Edit"
|
||||
)
|
||||
}
|
||||
IconButton({ showDeleteDialog=true }) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
EventDropdownMenu(
|
||||
{showDeleteDialog=true},
|
||||
{showDataCopyDialog=true},
|
||||
{
|
||||
onTaskEvents(TaskEvents.UpsertTask(
|
||||
context,
|
||||
EventModel(title = "Clone $title", description = description, recurrence = event.recurrence, endDateTime = event.endDateTime, notificationOffset = event.notificationOffset, workspaceId = event.workspaceId)
|
||||
))
|
||||
Toast.makeText(context, cloneString, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -185,6 +200,46 @@ fun EventDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(showDataCopyDialog){
|
||||
DataCopyDialog(
|
||||
workspaces.filterNot { it.workspaceId == workspaceId },
|
||||
{ dataCopyType, selectedWorkspaces ->
|
||||
if(selectedWorkspaces.isEmpty()) return@DataCopyDialog
|
||||
when(dataCopyType){
|
||||
DataCopyType.COPY -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(3)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 3)))
|
||||
}
|
||||
onTaskEvents(TaskEvents.UpsertTask(
|
||||
context,
|
||||
EventModel(title = title, description = description, recurrence = event.recurrence, endDateTime = event.endDateTime, notificationOffset = event.notificationOffset, workspaceId = workspace.workspaceId)
|
||||
))
|
||||
}
|
||||
|
||||
Toast.makeText(context, contentCopiedString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
DataCopyType.MOVE -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(3)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 3)))
|
||||
}
|
||||
|
||||
onTaskEvents(TaskEvents.UpsertTask(
|
||||
context,
|
||||
EventModel(title = title, description = description, recurrence = event.recurrence, endDateTime = event.endDateTime, notificationOffset = event.notificationOffset, workspaceId = workspace.workspaceId)
|
||||
))
|
||||
}
|
||||
|
||||
navController.popBackStack()
|
||||
Toast.makeText(context, contentMovedString, Toast.LENGTH_SHORT).show()
|
||||
onTaskEvents(TaskEvents.DeleteTask(event, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { showDataCopyDialog = false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.flux.ui.events.TaskEvents
|
||||
import com.flux.ui.screens.workspaces.SpacesToolBar
|
||||
import com.flux.ui.state.EventState
|
||||
import com.flux.ui.state.Settings
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
@@ -89,6 +90,12 @@ fun EventScreen(
|
||||
val datedEvents = state.datedEvents
|
||||
.filter { it.workspaceId==workspaceId }
|
||||
.filter { it.title.contains(query, ignoreCase = true) }
|
||||
.sortedBy {
|
||||
Instant.ofEpochMilli(it.startDateTime)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalTime()
|
||||
.toSecondOfDay()
|
||||
}
|
||||
val monthlyEventCount = computeMonthlyEventDates(state.allEvent.filter { it.workspaceId == workspaceId }, selectedMonth)
|
||||
val allEventInstances = state.allEventInstances
|
||||
var showSearchBar by remember { mutableStateOf(false) }
|
||||
@@ -153,8 +160,9 @@ fun EventScreen(
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(12.dp)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection))
|
||||
{
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
item {
|
||||
if(showSearchBar){ SpaceSearchBar(query, { query=it }, { showSearchBar=false }) }
|
||||
else {
|
||||
@@ -205,12 +213,14 @@ fun EventScreen(
|
||||
onMonthChange = { onEvent(TaskEvents.ChangeMonth(it)) },
|
||||
onDateChange = { onEvent(TaskEvents.ChangeDate(it)) }
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
DailyViewCalendar(selectedMonth, selectedDate){
|
||||
onEvent(TaskEvents.ChangeDate(it))
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
if(datedEvents.isEmpty()) item { EmptyEvents() }
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.flux.ui.screens.events
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -53,6 +54,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
@@ -250,28 +252,50 @@ fun NewEvent(
|
||||
stringResource(R.string.sunday_short)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(6.dp)) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
maxItemsInEachRow = 7
|
||||
) {
|
||||
weekdays.forEachIndexed { index, day ->
|
||||
val isSelected = index in rule.daysOfWeek
|
||||
|
||||
Card(
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
8.dp
|
||||
),
|
||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
|
||||
containerColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp),
|
||||
|
||||
contentColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = day,
|
||||
modifier = Modifier.padding(6.dp).fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(Modifier.fillMaxWidth())
|
||||
|
||||
@@ -56,7 +56,6 @@ import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -114,10 +113,8 @@ import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import kotlin.collections.filter
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
@@ -125,9 +122,11 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.flux.data.model.isCounted
|
||||
import com.flux.data.model.isLive
|
||||
@@ -162,7 +161,9 @@ fun TimerDialog(
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(shape = RoundedCornerShape(24.dp)) {
|
||||
Column(Modifier.padding(16.dp).fillMaxWidth()) {
|
||||
Column(Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()) {
|
||||
NumberPickerRow(Modifier.fillMaxWidth(), hours, minutes, { hours = it }) {
|
||||
minutes = it
|
||||
}
|
||||
@@ -213,6 +214,7 @@ fun CountedHabitStatus(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
@@ -230,25 +232,26 @@ fun CountedHabitStatus(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { completion },
|
||||
modifier = Modifier.size(120.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
strokeWidth = 12.dp,
|
||||
trackColor = MaterialTheme.colorScheme.primary.copy(0.35f),
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { completion },
|
||||
modifier = Modifier.size(120.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
strokeWidth = 12.dp,
|
||||
trackColor = MaterialTheme.colorScheme.primary.copy(0.35f),
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
|
||||
HabitInfoComponent(
|
||||
Icons.Outlined.IncompleteCircle,
|
||||
stringResource(R.string.current),
|
||||
@@ -270,85 +273,57 @@ fun CountedHabitStatus(
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
|
||||
val count = maxOf(currentCount - 1, 0)
|
||||
|
||||
val newInstance = HabitInstanceModel(
|
||||
instanceDate = todayEpoch,
|
||||
habitId = habit.id,
|
||||
workspaceId = habit.workspaceId,
|
||||
count = count
|
||||
)
|
||||
|
||||
onHabitEvents(
|
||||
HabitEvents.UpdateInstance(
|
||||
newInstance,
|
||||
habit.habitConfig
|
||||
)
|
||||
)
|
||||
val newInstance = HabitInstanceModel( instanceDate = todayEpoch, habitId = habit.id, workspaceId = habit.workspaceId, count = count )
|
||||
onHabitEvents( HabitEvents.UpdateInstance( newInstance, habit.habitConfig ) )
|
||||
},
|
||||
modifier = Modifier.weight(0.5f)
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Remove,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
Icon(
|
||||
Icons.Default.Remove,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.decrement))
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.decrement),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
)
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
|
||||
val count = currentCount + 1
|
||||
|
||||
val newInstance = HabitInstanceModel(
|
||||
instanceDate = todayEpoch,
|
||||
habitId = habit.id,
|
||||
workspaceId = habit.workspaceId,
|
||||
count = count
|
||||
)
|
||||
|
||||
onHabitEvents(
|
||||
HabitEvents.UpdateInstance(
|
||||
newInstance,
|
||||
habit.habitConfig
|
||||
)
|
||||
)
|
||||
val newInstance = HabitInstanceModel( instanceDate = todayEpoch, habitId = habit.id, workspaceId = habit.workspaceId, count = count )
|
||||
onHabitEvents( HabitEvents.UpdateInstance( newInstance, habit.habitConfig ) )
|
||||
},
|
||||
modifier = Modifier.weight(0.5f)
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.increment))
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.increment),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,7 +378,9 @@ fun TimedHabitStatus (
|
||||
)
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -450,7 +427,9 @@ fun TimedHabitStatus (
|
||||
}
|
||||
}
|
||||
|
||||
NumberPickerRow(Modifier.fillMaxWidth().padding(horizontal = 32.dp), hours, minutes, {hours=it}) { minutes=it }
|
||||
NumberPickerRow(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp), hours, minutes, {hours=it}) { minutes=it }
|
||||
|
||||
FilledTonalButton(
|
||||
{
|
||||
@@ -464,7 +443,9 @@ fun TimedHabitStatus (
|
||||
)
|
||||
onHabitEvents(HabitEvents.UpdateInstance(newInstance, habit.habitConfig))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
@@ -645,7 +626,9 @@ fun TimedHabitDialog(
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(shape = RoundedCornerShape(24.dp)) {
|
||||
Column(Modifier.padding(16.dp).fillMaxWidth()) {
|
||||
Column(Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.padding(vertical = 6.dp)) {
|
||||
options.forEachIndexed { index, label ->
|
||||
SegmentedButton(
|
||||
@@ -662,7 +645,9 @@ fun TimedHabitDialog(
|
||||
}
|
||||
|
||||
NumberPickerRow(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 32.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
hours,
|
||||
minutes,
|
||||
{ hours=it }
|
||||
@@ -728,10 +713,22 @@ fun HabitDetailedInfo(
|
||||
)
|
||||
}
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val columns = when {
|
||||
density.fontScale > 1.5f -> 1
|
||||
|
||||
configuration.screenWidthDp < 360 -> 1
|
||||
configuration.screenWidthDp < 480 -> 2
|
||||
|
||||
else -> 3
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
columns = GridCells.Fixed(columns),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 1000.dp)
|
||||
@@ -847,22 +844,47 @@ fun HabitDetailedInfo(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HabitInfoComponent(icon: ImageVector, title: String, description: String){
|
||||
fun HabitInfoComponent(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
description: String
|
||||
) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
CircleWrapper(MaterialTheme.colorScheme.primaryContainer) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(title, modifier = Modifier.alpha(0.85f), style = MaterialTheme.typography.labelMedium)
|
||||
Text(description, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.labelMedium)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.alpha(0.85f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(2.dp))
|
||||
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -967,7 +989,7 @@ fun HabitDateCard(
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = shapeManager(radius = radius * 2),
|
||||
shape = shapeManager(radius = radius),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor
|
||||
@@ -981,13 +1003,13 @@ fun HabitDateCard(
|
||||
) {
|
||||
Text(
|
||||
day.uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.ExtraLight),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.alpha(0.95f)
|
||||
)
|
||||
Text(
|
||||
date.toString(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.ExtraLight),
|
||||
modifier = Modifier.alpha(0.95f)
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.ExtraLight),
|
||||
modifier = Modifier.alpha(0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1140,7 +1162,7 @@ fun OtherConfigCard(
|
||||
val text = if (isTimed) { formatDuration(instance?.timeSpent ?: 0L) } else { (instance?.count ?: 0).toString() }
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = shapeManager(radius = radius * 2),
|
||||
shape = shapeManager(radius = radius),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor
|
||||
@@ -1150,25 +1172,24 @@ fun OtherConfigCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row{
|
||||
Text(
|
||||
"$date, ",
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.ExtraLight),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.alpha(0.95f)
|
||||
)
|
||||
|
||||
Text(
|
||||
day.uppercase(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.ExtraLight),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.alpha(0.95f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.ExtraLight),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.ExtraLight),
|
||||
modifier = Modifier.alpha(0.85f)
|
||||
)
|
||||
}
|
||||
@@ -1214,14 +1235,13 @@ fun HabitCalendarCard(
|
||||
|
||||
)
|
||||
val today = LocalDate.now()
|
||||
val habitStartDate =
|
||||
Instant.ofEpochMilli(startDateTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
val habitEndEpochDay = if (endDateTime == -1L) null
|
||||
else Instant.ofEpochMilli(endDateTime)
|
||||
val habitStartDate = Instant.ofEpochMilli(startDateTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
val habitEndEpochDay = if (endDateTime == -1L) null else Instant.ofEpochMilli(endDateTime)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.toEpochDay()
|
||||
val locale = LocalLocale.current.platformLocale
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1275,14 +1295,14 @@ fun HabitCalendarCard(
|
||||
}
|
||||
|
||||
// Days of week row
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(Modifier.fillMaxWidth().padding(bottom = 6.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
daysOfWeek.forEach {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold)
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1423,7 +1443,7 @@ fun WeeklyHabitAnalyticsCard(
|
||||
stringResource(R.string.sunday_short)
|
||||
)
|
||||
|
||||
val dayStatus = daysOfWeek.mapIndexed { index, _ ->
|
||||
val dayStatus = List(daysOfWeek.size) { index ->
|
||||
val date = startOfWeek.plusDays(index.toLong())
|
||||
val epoch = date.toEpochDay()
|
||||
|
||||
@@ -2108,7 +2128,9 @@ fun HabitConfigCard(
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.clip(RoundedCornerShape(50)).padding(horizontal = 2.dp),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(50))
|
||||
.padding(horizontal = 2.dp),
|
||||
shape = RoundedCornerShape(50),
|
||||
onClick = onClick,
|
||||
colors = CardDefaults.cardColors(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.flux.ui.screens.habits
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -12,15 +13,21 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flux.R
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.data.model.HabitConfig
|
||||
import com.flux.data.model.HabitInstanceModel
|
||||
import com.flux.data.model.HabitModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.ui.common.DataCopyDialog
|
||||
import com.flux.ui.common.DeleteAlert
|
||||
import com.flux.ui.common.HabitScaffold
|
||||
import com.flux.ui.events.HabitEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -30,13 +37,20 @@ fun HabitDetails(
|
||||
radius: Int,
|
||||
workspaceId: String,
|
||||
habit: HabitModel,
|
||||
workspaces: List<WorkspaceModel>,
|
||||
habitInstances: List<HabitInstanceModel>,
|
||||
onHabitEvents: (HabitEvents) -> Unit
|
||||
onHabitEvents: (HabitEvents) -> Unit,
|
||||
onWorkspaceEvents: (WorkspaceEvents) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
val todayEpoch = LocalDate.now().toEpochDay()
|
||||
val todayInstance = habitInstances.firstOrNull { it.instanceDate == todayEpoch }
|
||||
val isAllowedByRecurrence = isDateAllowedForHabit(habit.recurrence, todayEpoch)
|
||||
var showDataCopyDialog by remember { mutableStateOf(false) }
|
||||
val cloneString = stringResource(R.string.clone_created_successfully)
|
||||
val contentCopiedString = stringResource(R.string.content_copied)
|
||||
val contentMovedString = stringResource(R.string.content_moved)
|
||||
|
||||
if(showDeleteDialog){
|
||||
DeleteAlert({
|
||||
@@ -52,16 +66,21 @@ fun HabitDetails(
|
||||
onBackPressed = { navController.popBackStack() },
|
||||
onDeleteClicked = { showDeleteDialog=true },
|
||||
onEditClicked = { navController.navigate(NavRoutes.NewHabit.withArgs(workspaceId, habit.id)) },
|
||||
onCloneNote = {
|
||||
onHabitEvents(HabitEvents.UpsertHabit(context, HabitModel(workspaceId = habit.workspaceId, habitConfig = habit.habitConfig, title = "Clone ${habit.title}", description = habit.description, endDateTime = habit.endDateTime, notificationOffset = habit.notificationOffset, recurrence = habit.recurrence)))
|
||||
Toast.makeText(context, cloneString, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onCopyNote = { showDataCopyDialog=true },
|
||||
content = { innerPadding ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { HabitDetailedInfo(radius, habit, habitInstances) }
|
||||
if(habit.habitConfig is HabitConfig.Counted) {
|
||||
if(habit.habitConfig is HabitConfig.Counted && isAllowedByRecurrence) {
|
||||
item { CountedHabitStatus(radius, habit, todayInstance, onHabitEvents) }
|
||||
}
|
||||
if(habit.habitConfig is HabitConfig.Timed) {
|
||||
@@ -77,4 +96,39 @@ fun HabitDetails(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if(showDataCopyDialog){
|
||||
DataCopyDialog(
|
||||
workspaces.filterNot { it.workspaceId == workspaceId },
|
||||
{ dataCopyType, selectedWorkspaces ->
|
||||
if(selectedWorkspaces.isEmpty()) return@DataCopyDialog
|
||||
when(dataCopyType){
|
||||
DataCopyType.COPY -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(5)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 5)))
|
||||
}
|
||||
onHabitEvents(HabitEvents.UpsertHabit(context, HabitModel(workspaceId = workspace.workspaceId, habitConfig = habit.habitConfig, title = habit.title, description = habit.description, endDateTime = habit.endDateTime, notificationOffset = habit.notificationOffset, recurrence = habit.recurrence)))
|
||||
}
|
||||
|
||||
Toast.makeText(context, contentCopiedString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
DataCopyType.MOVE -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(5)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 5)))
|
||||
}
|
||||
|
||||
onHabitEvents(HabitEvents.UpsertHabit(context, HabitModel(workspaceId = workspace.workspaceId, habitConfig = habit.habitConfig, title = habit.title, description = habit.description, endDateTime = habit.endDateTime, notificationOffset = habit.notificationOffset, recurrence = habit.recurrence)))
|
||||
|
||||
}
|
||||
|
||||
navController.popBackStack()
|
||||
Toast.makeText(context, contentMovedString, Toast.LENGTH_SHORT).show()
|
||||
onHabitEvents(HabitEvents.DeleteHabit(habit, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { showDataCopyDialog = false }
|
||||
}
|
||||
}
|
||||
@@ -234,5 +234,6 @@ fun isDateAllowedForHabit(recurrence: RecurrenceRule, epochDay: Long): Boolean {
|
||||
is RecurrenceRule.Once -> true
|
||||
is RecurrenceRule.Monthly -> true
|
||||
is RecurrenceRule.Yearly -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -68,6 +69,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
@@ -273,11 +275,14 @@ fun NewHabit(
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
Modifier
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
.padding(6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
maxItemsInEachRow = 7
|
||||
) {
|
||||
weekdays.forEachIndexed { index, day ->
|
||||
val isSelected = selectedDays.contains(index)
|
||||
Card(
|
||||
@@ -291,22 +296,30 @@ fun NewHabit(
|
||||
selectedDays.add(index)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
6.dp
|
||||
),
|
||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
|
||||
containerColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp),
|
||||
|
||||
contentColor =
|
||||
if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = day,
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,18 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.flux.R
|
||||
import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.TodoItem
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.ui.common.DataCopyDialog
|
||||
import com.flux.ui.common.DatePickerModal
|
||||
import com.flux.ui.events.NotesEvents
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
import com.flux.ui.screens.notes.LinkDialog
|
||||
import com.flux.ui.screens.notes.ListDialog
|
||||
import com.flux.ui.screens.notes.MarkdownEditorRow
|
||||
@@ -107,6 +117,8 @@ import com.flux.ui.screens.notes.TaskItem
|
||||
@Composable
|
||||
fun EditJournal(
|
||||
navController: NavController,
|
||||
workspaces: List<WorkspaceModel>,
|
||||
workspaceId: String,
|
||||
journal: JournalModel,
|
||||
outline: HeaderNode,
|
||||
aboutJournal: TextState,
|
||||
@@ -118,7 +130,10 @@ fun EditJournal(
|
||||
allLabels: List<LabelModel>,
|
||||
journalViewModel: JournalViewModel,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
onJournalEvents: (JournalEvents) -> Unit
|
||||
onJournalEvents: (JournalEvents) -> Unit,
|
||||
onNotesEvents: (NotesEvents) -> Unit,
|
||||
onTodoEvents: (TodoEvents) -> Unit,
|
||||
onWorkspaceEvents: (WorkspaceEvents) -> Unit
|
||||
) {
|
||||
val textFieldStateSaver = Saver<TextFieldState, String>(
|
||||
save = { it.text.toString() },
|
||||
@@ -131,7 +146,7 @@ fun EditJournal(
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val hasContent = remember(journal.journalId) { journal.text.isNotBlank() }
|
||||
|
||||
val currentWorkspace = workspaces.find { it.workspaceId == workspaceId }
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = if (startWithReadView && hasContent) 1 else 0,
|
||||
pageCount = { 2 }
|
||||
@@ -158,8 +173,15 @@ fun EditJournal(
|
||||
addAll(journal.labels)
|
||||
}
|
||||
}
|
||||
var showDataCopyDialog by remember { mutableStateOf(false) }
|
||||
var showConvertDialog by remember { mutableStateOf(false) }
|
||||
val recorder = AudioRecorder(context)
|
||||
|
||||
val cloneString = stringResource(R.string.clone_created_successfully)
|
||||
val contentCopiedString = stringResource(R.string.content_copied)
|
||||
val contentMovedString = stringResource(R.string.content_moved)
|
||||
val successString = stringResource(R.string.success)
|
||||
|
||||
val rootPicker =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
@@ -207,7 +229,7 @@ fun EditJournal(
|
||||
|
||||
val hasChanged = newText != journal.text
|
||||
|
||||
if (!hasChanged && currentLabelIds.toList()==journal.labels) return
|
||||
if (!hasChanged && currentLabelIds.toList()==journal.labels && journalDate==journal.dateTime) return
|
||||
|
||||
onJournalEvents(
|
||||
JournalEvents.UpsertEntry(
|
||||
@@ -249,7 +271,13 @@ fun EditJournal(
|
||||
onShareNote = { showShareDialog = true },
|
||||
onSaveNote = { showSaveDialog = true },
|
||||
onAddLabel = { showLabelDialog = true },
|
||||
onPrintNote = { printPdf(context as Activity, readWebView, convertMillisToDate(journal.dateTime)+"_"+ convertMillisToTime(journal.dateTime)) }
|
||||
onPrintNote = { printPdf(context as Activity, readWebView, convertMillisToDate(journal.dateTime)+"_"+ convertMillisToTime(journal.dateTime)) },
|
||||
onCloneNote = {
|
||||
onJournalEvents(JournalEvents.UpsertEntry(JournalModel(text = contentState.text.toString(), workspaceId = workspaceId, labels = currentLabelIds)))
|
||||
Toast.makeText(context, cloneString, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onCopyNote = { showDataCopyDialog = true },
|
||||
onConvertNote = { showConvertDialog = true }
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -548,6 +576,82 @@ fun EditJournal(
|
||||
)
|
||||
}
|
||||
|
||||
if(showDataCopyDialog){
|
||||
DataCopyDialog(
|
||||
workspaces.filterNot { it.workspaceId == workspaceId },
|
||||
{ dataCopyType, selectedWorkspaces ->
|
||||
if(selectedWorkspaces.isEmpty()) return@DataCopyDialog
|
||||
when(dataCopyType){
|
||||
DataCopyType.COPY -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(4)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 4)))
|
||||
}
|
||||
onJournalEvents(JournalEvents.UpsertEntry(JournalModel(text = contentState.text.toString(), workspaceId = workspace.workspaceId)))
|
||||
}
|
||||
|
||||
Toast.makeText(context, contentCopiedString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
DataCopyType.MOVE -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(4)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 4)))
|
||||
}
|
||||
onJournalEvents(JournalEvents.UpsertEntry(JournalModel(text = contentState.text.toString(), workspaceId = workspace.workspaceId)))
|
||||
}
|
||||
|
||||
navController.popBackStack()
|
||||
Toast.makeText(context, contentMovedString, Toast.LENGTH_SHORT).show()
|
||||
onJournalEvents(JournalEvents.DeleteEntry(journal))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { showDataCopyDialog = false }
|
||||
}
|
||||
|
||||
if(showConvertDialog){
|
||||
ConvertJournalDialog ({ type ->
|
||||
when(type){
|
||||
ConvertType.TODO -> {
|
||||
val todo = TodoModel(
|
||||
workspaceId = journal.workspaceId,
|
||||
title = "Journal ${convertMillisToDate(System.currentTimeMillis())}",
|
||||
items = contentState.text.toString()
|
||||
.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { line ->
|
||||
TodoItem(value = line)
|
||||
}
|
||||
.toList()
|
||||
)
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(2)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = currentWorkspace.selectedSpaces + 2)))
|
||||
}
|
||||
onTodoEvents(TodoEvents.UpsertList(context, false, todo))
|
||||
|
||||
navController.popBackStack()
|
||||
onJournalEvents(JournalEvents.DeleteEntry(journal))
|
||||
}
|
||||
ConvertType.NOTE -> {
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(1)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = currentWorkspace.selectedSpaces + 1)))
|
||||
}
|
||||
onNotesEvents(NotesEvents.UpsertNote(NotesModel(title = "Journal ${convertMillisToDate(System.currentTimeMillis())}", description = contentState.text.toString(), workspaceId = workspaceId, labels = currentLabelIds)))
|
||||
|
||||
navController.popBackStack()
|
||||
onJournalEvents(JournalEvents.DeleteEntry(journal))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
Toast.makeText(context, successString, Toast.LENGTH_SHORT).show()
|
||||
showConvertDialog=false
|
||||
}) {
|
||||
showConvertDialog=false
|
||||
}
|
||||
}
|
||||
|
||||
NotesInfoBottomSheet(
|
||||
isVisible = showAboutNotes,
|
||||
words = aboutJournal.wordCountWithPunctuation,
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LabelImportant
|
||||
import androidx.compose.material.icons.automirrored.outlined.Note
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -60,8 +62,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.flux.R
|
||||
import com.flux.data.model.LabelModel
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.other.parseMarkdownContent
|
||||
import com.flux.ui.common.CategoryRow
|
||||
import com.flux.ui.common.DateOptionRow
|
||||
@@ -71,6 +75,7 @@ import com.flux.ui.common.FilterOption
|
||||
import com.flux.ui.common.MultiOptionRow
|
||||
import com.flux.ui.common.OptionRow
|
||||
import com.flux.ui.common.SelectionType
|
||||
import com.flux.ui.screens.notes.ExportCard
|
||||
import com.flux.ui.screens.settings.shapeManager
|
||||
import kotlin.collections.set
|
||||
|
||||
@@ -425,3 +430,31 @@ fun JournalFilterSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConvertJournalDialog(
|
||||
onConfirm: (ConvertType) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.convert),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
ExportCard(Icons.AutoMirrored.Outlined.Note, stringResource(R.string.convert_to_todo)) { onConfirm(ConvertType.TODO) }
|
||||
ExportCard(Icons.Outlined.AutoStories, stringResource(R.string.convert_to_note)) { onConfirm(ConvertType.NOTE) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ import com.flux.ui.state.JournalState
|
||||
import com.flux.ui.state.Settings
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
data class FilterState(
|
||||
val sort: String? = null,
|
||||
@@ -95,6 +97,7 @@ fun JournalScreen(
|
||||
){
|
||||
val workspaceId = workspace.workspaceId
|
||||
val isLoading = state.isLoading
|
||||
val is24HoursFormat = settings.data.is24HourFormat
|
||||
val radius = settings.data.cornerRadius
|
||||
val context = LocalContext.current
|
||||
var query by remember { mutableStateOf("") }
|
||||
@@ -221,7 +224,7 @@ fun JournalScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val buttonModifier = Modifier.width(120.dp)
|
||||
val buttonModifier = Modifier.width(140.dp)
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = buttonModifier,
|
||||
@@ -311,7 +314,14 @@ fun JournalScreen(
|
||||
}
|
||||
if(allEntries.isEmpty()) item { EmptyJournal() }
|
||||
items(allEntries) { entry->
|
||||
JournalCardHeader(convertMillisToDate(entry.dateTime) + ", " + convertMillisToTime(entry.dateTime))
|
||||
JournalCardHeader("${convertMillisToDay(entry.dateTime)}, ${
|
||||
convertMillisToDate(entry.dateTime)
|
||||
}, ${
|
||||
convertMillisToTime(
|
||||
entry.dateTime,
|
||||
is24Hour = is24HoursFormat
|
||||
)
|
||||
}")
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
TimelineBody(isLast = false)
|
||||
JournalPreview(radius, entry.text, allLabels.filter { entry.labels.contains(it.labelId) }) {
|
||||
@@ -346,3 +356,9 @@ fun JournalScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun convertMillisToDay(millis: Long): String {
|
||||
return Instant.ofEpochMilli(millis)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.dayOfWeek
|
||||
.getDisplayName(TextStyle.FULL, Locale.getDefault())
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.automirrored.outlined.LabelImportant
|
||||
import androidx.compose.material.icons.automirrored.outlined.List
|
||||
import androidx.compose.material.icons.automirrored.outlined.Note
|
||||
import androidx.compose.material.icons.filled.Abc
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
@@ -73,6 +74,7 @@ import androidx.compose.material.icons.filled.TextFields
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.outlined.AddChart
|
||||
import androidx.compose.material.icons.outlined.AudioFile
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.CheckBox
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.DataArray
|
||||
@@ -166,7 +168,6 @@ import com.flux.other.AudioRecorder
|
||||
import com.flux.other.Constants
|
||||
import com.flux.other.ExportType
|
||||
import com.flux.other.HeaderNode
|
||||
import com.flux.other.parseMarkdownContent
|
||||
import com.flux.ui.screens.settings.ActionType
|
||||
import com.flux.ui.screens.settings.CircleWrapper
|
||||
import com.flux.ui.screens.settings.SettingOption
|
||||
@@ -180,6 +181,10 @@ import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.other.MarkdownBlock
|
||||
import com.flux.other.MediaChipsRow
|
||||
import com.flux.other.extractMedia
|
||||
import com.flux.ui.common.CategoryRow
|
||||
import com.flux.ui.common.FilterCategory
|
||||
import com.flux.ui.common.FilterOption
|
||||
@@ -187,6 +192,7 @@ import com.flux.ui.common.MultiOptionRow
|
||||
import com.flux.ui.common.OptionRow
|
||||
import com.flux.ui.common.SelectionType
|
||||
import kotlin.collections.filter
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// ------------- Dialogs -------------
|
||||
@Composable
|
||||
@@ -436,6 +442,34 @@ fun ShareDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConvertNotesDialog(
|
||||
onConfirm: (ConvertType) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.convert),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
ExportCard(Icons.AutoMirrored.Outlined.Note, stringResource(R.string.convert_to_todo)) { onConfirm(ConvertType.TODO) }
|
||||
ExportCard(Icons.Outlined.AutoStories, stringResource(R.string.convert_to_journal)) { onConfirm(ConvertType.JOURNAL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExportCard(icon: ImageVector, title: String, onClick: () -> Unit){
|
||||
Card(
|
||||
@@ -1041,7 +1075,7 @@ fun StudioRecorderUI(
|
||||
|
||||
elapsedMs = System.currentTimeMillis() - start
|
||||
|
||||
delay(50)
|
||||
delay(50.milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1711,12 +1745,22 @@ fun NotesFilterSheet(
|
||||
fun NotesPreviewCard(
|
||||
modifier: Modifier = Modifier,
|
||||
radius: Int,
|
||||
notesPreviewMode: Int,
|
||||
isSelected: Boolean,
|
||||
note: NotesModel,
|
||||
labels: List<String>,
|
||||
onClick: (String) -> Unit,
|
||||
onLongPressed: () -> Unit
|
||||
) {
|
||||
// Extract media once — MarkdownBlock will use the cleaned text internally,
|
||||
// and we render the chips ourselves at the bottom of the card.
|
||||
val mediaExtraction = remember(note.description) { extractMedia(note.description) }
|
||||
val maxHeight = when(notesPreviewMode) {
|
||||
0 -> 0.dp
|
||||
1 -> 180.dp
|
||||
else -> 360.dp
|
||||
}
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)),
|
||||
modifier = modifier
|
||||
@@ -1735,6 +1779,7 @@ fun NotesPreviewCard(
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = note.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
@@ -1746,16 +1791,33 @@ fun NotesPreviewCard(
|
||||
.padding(top = 12.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = parseMarkdownContent(note.description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 12,
|
||||
modifier = Modifier
|
||||
.alpha(0.9f)
|
||||
.padding(horizontal = 12.dp)
|
||||
)
|
||||
// Description — media already stripped inside MarkdownBlock
|
||||
if(maxHeight!=0.dp){
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = maxHeight)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
MarkdownBlock(
|
||||
text = note.description,
|
||||
onClick = { onClick(note.notesId) },
|
||||
onLongClick = onLongPressed
|
||||
)
|
||||
}
|
||||
|
||||
// Media chips pinned here, below the text, above labels
|
||||
if (!mediaExtraction.media.isEmpty) {
|
||||
MediaChipsRow(
|
||||
media = mediaExtraction.media,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
onClick = { onClick(note.notesId) },
|
||||
onLongClick = onLongPressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Labels
|
||||
val maxVisibleLabels = 2
|
||||
val visibleLabels = labels.take(maxVisibleLabels)
|
||||
val extraCount = labels.size - maxVisibleLabels
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.flux.ui.screens.notes
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
@@ -71,8 +72,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastJoinToString
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.JournalModel
|
||||
import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.TodoItem
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.Constants
|
||||
import com.flux.other.HeaderNode
|
||||
@@ -91,12 +96,19 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.flux.other.AudioRecorder
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.ui.common.DataCopyDialog
|
||||
import com.flux.ui.common.convertMillisToTime
|
||||
import com.flux.ui.events.JournalEvents
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NoteDetails(
|
||||
navController: NavController,
|
||||
workspaces: List<WorkspaceModel>,
|
||||
outline: HeaderNode,
|
||||
aboutNotes: TextState,
|
||||
workspaceId: String,
|
||||
@@ -109,7 +121,10 @@ fun NoteDetails(
|
||||
allLabels: List<LabelModel>,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
notesViewModel: NotesViewModel,
|
||||
onNotesEvents: (NotesEvents) -> Unit
|
||||
onNotesEvents: (NotesEvents) -> Unit,
|
||||
onJournalEvents: (JournalEvents) -> Unit,
|
||||
onTodoEvents: (TodoEvents) -> Unit,
|
||||
onWorkspaceEvents: (WorkspaceEvents) -> Unit
|
||||
) {
|
||||
val textFieldStateSaver = Saver<TextFieldState, String>(
|
||||
save = { it.text.toString() },
|
||||
@@ -140,14 +155,21 @@ fun NoteDetails(
|
||||
var showListDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showAudioRecorder by rememberSaveable { mutableStateOf(false) }
|
||||
var showSelectLabels by rememberSaveable { mutableStateOf(false) }
|
||||
var showDataCopyDialog by remember { mutableStateOf(false) }
|
||||
var showConvertDialog by remember { mutableStateOf(false) }
|
||||
var isPinned by rememberSaveable(note.notesId) { mutableStateOf(note.isPinned) }
|
||||
val noteLabelIds = rememberSaveable {
|
||||
mutableStateListOf<String>().apply {
|
||||
addAll(note.labels)
|
||||
}
|
||||
}
|
||||
val currentWorkspace = workspaces.find { it.workspaceId == workspaceId }
|
||||
val isReadView by remember { derivedStateOf { pagerState.currentPage == 1 } }
|
||||
var readWebView by remember { mutableStateOf<WebView?>(null) }
|
||||
val cloneString = stringResource(R.string.clone_created_successfully)
|
||||
val contentCopiedString = stringResource(R.string.content_copied)
|
||||
val contentMovedString = stringResource(R.string.content_moved)
|
||||
val successString = stringResource(R.string.success)
|
||||
|
||||
LaunchedEffect(searchState.searchWord, contentState.text) {
|
||||
withContext(Dispatchers.Default) {
|
||||
@@ -255,7 +277,13 @@ fun NoteDetails(
|
||||
settingsViewModel = settingsViewModel,
|
||||
rootPicker = rootPicker
|
||||
) { showSaveNotesDialog = true } },
|
||||
onPrintNote = { printPdf(context as Activity, readWebView, titleState.text.toString()) }
|
||||
onPrintNote = { printPdf(context as Activity, readWebView, titleState.text.toString()) },
|
||||
onCloneNote = {
|
||||
onNotesEvents(NotesEvents.UpsertNote(NotesModel(title = "Clone ${titleState.text}", description = contentState.text.toString(), workspaceId = workspaceId, labels = noteLabelIds)))
|
||||
Toast.makeText(context, cloneString, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onConvertNote = { showConvertDialog = true },
|
||||
onCopyNote = { showDataCopyDialog = true }
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -487,12 +515,90 @@ fun NoteDetails(
|
||||
}
|
||||
}
|
||||
|
||||
if(showDataCopyDialog){
|
||||
DataCopyDialog(
|
||||
workspaces.filterNot { it.workspaceId == workspaceId },
|
||||
{ dataCopyType, selectedWorkspaces ->
|
||||
if(selectedWorkspaces.isEmpty()) return@DataCopyDialog
|
||||
when(dataCopyType){
|
||||
DataCopyType.COPY -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(1)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 1)))
|
||||
}
|
||||
onNotesEvents(NotesEvents.UpsertNote(NotesModel(title = titleState.text.toString(), description = contentState.text.toString(), workspaceId = workspace.workspaceId)))
|
||||
}
|
||||
|
||||
Toast.makeText(context, contentCopiedString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
DataCopyType.MOVE -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(1)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 1)))
|
||||
}
|
||||
onNotesEvents(NotesEvents.UpsertNote(NotesModel(title = titleState.text.toString(), description = contentState.text.toString(), workspaceId = workspace.workspaceId)))
|
||||
}
|
||||
|
||||
navController.popBackStack()
|
||||
Toast.makeText(context, contentMovedString, Toast.LENGTH_SHORT).show()
|
||||
onNotesEvents(NotesEvents.DeleteNote(note))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { showDataCopyDialog = false }
|
||||
}
|
||||
|
||||
if(showAudioRecorder){
|
||||
RecordAudioDialog(context, recorder, {onNotesEvents(NotesEvents.ImportAudio(context, it!!, contentState))}) {
|
||||
showAudioRecorder=false
|
||||
}
|
||||
}
|
||||
|
||||
if(showConvertDialog){
|
||||
ConvertNotesDialog ({ type ->
|
||||
when(type){
|
||||
ConvertType.TODO -> {
|
||||
val todo = TodoModel(
|
||||
workspaceId = note.workspaceId,
|
||||
title = titleState.text.toString(),
|
||||
items = contentState.text.toString()
|
||||
.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { line ->
|
||||
TodoItem(
|
||||
value = line
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
)
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(2)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = currentWorkspace.selectedSpaces + 2)))
|
||||
}
|
||||
onTodoEvents(TodoEvents.UpsertList(context, false, todo))
|
||||
|
||||
navController.popBackStack()
|
||||
onNotesEvents(NotesEvents.DeleteNote(note))
|
||||
}
|
||||
ConvertType.JOURNAL -> {
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(4)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = currentWorkspace.selectedSpaces + 4)))
|
||||
}
|
||||
onJournalEvents(JournalEvents.UpsertEntry(JournalModel(text = contentState.text.toString(), workspaceId = workspaceId, labels = noteLabelIds)))
|
||||
|
||||
navController.popBackStack()
|
||||
onNotesEvents(NotesEvents.DeleteNote(note))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
Toast.makeText(context, successString, Toast.LENGTH_SHORT).show()
|
||||
showConvertDialog=false
|
||||
}) {
|
||||
showConvertDialog=false
|
||||
}
|
||||
}
|
||||
|
||||
NotesInfoBottomSheet(
|
||||
isVisible = showAboutNotes,
|
||||
words = aboutNotes.wordCountWithPunctuation,
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.flux.R
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -101,6 +102,7 @@ fun NotesScreen(
|
||||
val isGridView = settings.data.isGridView
|
||||
val radius = settings.data.cornerRadius
|
||||
val isLoading = state.isLoading
|
||||
val notesPreviewMode = settings.data.notesPreviewMode
|
||||
val columns = if(isGridView) 2 else 1
|
||||
val selectedNotes = remember { mutableStateListOf<NotesModel>() }
|
||||
val importSuccess = stringResource(R.string.import_success)
|
||||
@@ -231,7 +233,7 @@ fun NotesScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val buttonModifier = Modifier.width(120.dp)
|
||||
val buttonModifier = Modifier.width(140.dp)
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = buttonModifier,
|
||||
@@ -275,10 +277,15 @@ fun NotesScreen(
|
||||
columns = StaggeredGridCells.Fixed(columns),
|
||||
verticalItemSpacing = 8.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 12.dp,
|
||||
bottom = 80.dp
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(12.dp)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
@@ -354,6 +361,7 @@ fun NotesScreen(
|
||||
radius = radius,
|
||||
isSelected = selectedNotes.contains(note),
|
||||
note = note,
|
||||
notesPreviewMode = notesPreviewMode,
|
||||
labels = allLabels.filter { note.labels.contains(it.labelId) }.map { it.value },
|
||||
onClick = { navController.navigate(NavRoutes.NoteDetails.withArgs(workspaceId, note.notesId)) },
|
||||
onLongPressed = {
|
||||
@@ -380,6 +388,7 @@ fun NotesScreen(
|
||||
radius = radius,
|
||||
isSelected = selectedNotes.contains(note),
|
||||
note = note,
|
||||
notesPreviewMode = notesPreviewMode,
|
||||
labels = allLabels.filter { note.labels.contains(it.labelId) }.map { it.value },
|
||||
onClick = { navController.navigate(NavRoutes.NoteDetails.withArgs(workspaceId, note.notesId)) },
|
||||
onLongPressed = {
|
||||
|
||||
@@ -49,7 +49,7 @@ data class MarkdownStyles(
|
||||
fun fromColorScheme(colorScheme: ColorScheme) = MarkdownStyles(
|
||||
hexTextColor = colorScheme.onSurface.toArgb().toHexColor(),
|
||||
hexCodeBackgroundColor = colorScheme.surfaceVariant.toArgb().toHexColor(),
|
||||
hexPreBackgroundColor = colorScheme.surfaceColorAtElevation(1.dp).toArgb().toHexColor(),
|
||||
hexPreBackgroundColor = colorScheme.surfaceVariant.toArgb().toHexColor(),
|
||||
hexQuoteBackgroundColor = colorScheme.secondaryContainer.toArgb().toHexColor(),
|
||||
hexLinkColor = colorScheme.primary.toArgb().toHexColor(),
|
||||
hexBorderColor = colorScheme.outline.toArgb().toHexColor(),
|
||||
|
||||
@@ -41,7 +41,7 @@ fun TextFieldBuffer.inlineBrackets() = inlineWrap("[", "]")
|
||||
|
||||
fun TextFieldBuffer.inlineBraces() = inlineWrap("{", "}")
|
||||
|
||||
fun TextFieldBuffer.inlineCode() = inlineWrap("`")
|
||||
fun TextFieldBuffer.inlineCode() = inlineWrap("```", "```")
|
||||
|
||||
fun TextFieldBuffer.inlineMath() = inlineWrap("$")
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.flux.ui.screens.progressBoard
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -19,7 +21,9 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Flag
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Timelapse
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -28,6 +32,7 @@ import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
@@ -50,6 +55,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -67,6 +73,10 @@ import com.flux.ui.screens.settings.shapeManager
|
||||
import com.flux.ui.theme.completed
|
||||
import com.flux.ui.theme.failed
|
||||
import com.flux.ui.theme.pending
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -78,6 +88,7 @@ fun NewBoardItemSheet(
|
||||
onConfirm: (ProgressBoardModel) -> Unit,
|
||||
onDelete: (ProgressBoardModel) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusRequesterDesc = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var selectedStatus by remember(progressBoardItem) { mutableIntStateOf(progressBoardItem.status) }
|
||||
@@ -90,13 +101,50 @@ fun NewBoardItemSheet(
|
||||
var isChangeIcon by remember { mutableStateOf(false) }
|
||||
val iconSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var newIcon by remember(progressBoardItem) { mutableIntStateOf(progressBoardItem.icon) }
|
||||
val startDateString = stringResource(R.string.start_date_after_target_error)
|
||||
val targetDateString = stringResource(R.string.target_date_before_start_error)
|
||||
|
||||
if(showDateSelector){
|
||||
DatePickerModal({
|
||||
if (isSelectingStartDate) startDate=it?:-1L
|
||||
else endDate=it?:-1L
|
||||
}) {
|
||||
showDateSelector=false
|
||||
if (showDateSelector) {
|
||||
DatePickerModal(
|
||||
{
|
||||
val selectedDate = it ?: -1L
|
||||
|
||||
if (isSelectingStartDate) {
|
||||
|
||||
if (
|
||||
endDate != -1L &&
|
||||
selectedDate != -1L &&
|
||||
selectedDate > endDate
|
||||
) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
startDateString,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
startDate = selectedDate
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (
|
||||
startDate != -1L &&
|
||||
selectedDate != -1L &&
|
||||
selectedDate < startDate
|
||||
) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
targetDateString,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
endDate = selectedDate
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
) {
|
||||
showDateSelector = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,13 +314,13 @@ fun BoardStatusItem(isSelected: Boolean, status: String, color: Color, onClick:
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(4.dp),
|
||||
Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(12.dp)
|
||||
.padding(start = 4.dp)
|
||||
.size(16.dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.then(
|
||||
if (isSelected) {
|
||||
@@ -365,7 +413,7 @@ fun BoardContainer(
|
||||
) {
|
||||
Text(item.title, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FlowRow(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (item.startDate != -1L) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -386,9 +434,14 @@ fun BoardContainer(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Flag, null, modifier = Modifier
|
||||
.size(16.dp)
|
||||
.alpha(0.75f))
|
||||
Icon(
|
||||
Icons.Default.Flag,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.alpha(0.75f)
|
||||
)
|
||||
|
||||
Text(
|
||||
convertMillisToDate(item.endDate),
|
||||
modifier = Modifier.alpha(0.75f),
|
||||
@@ -396,6 +449,45 @@ fun BoardContainer(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.endDate != -1L && item.status != 2) {
|
||||
|
||||
val daysLeft = daysLeft(item.endDate)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector =
|
||||
if (daysLeft < 0)
|
||||
Icons.Default.Warning
|
||||
else
|
||||
Icons.Default.Schedule,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.alpha(0.75f)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when {
|
||||
daysLeft > 1 -> stringResource(R.string.days_left, daysLeft)
|
||||
daysLeft == 1L -> stringResource(R.string.one_day_left)
|
||||
daysLeft == 0L -> stringResource(R.string.Today)
|
||||
daysLeft == -1L -> stringResource(R.string.one_day_overdue)
|
||||
else -> stringResource(R.string.days_overdue, -daysLeft)
|
||||
},
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = when {
|
||||
daysLeft < 0 -> MaterialTheme.colorScheme.error
|
||||
daysLeft <= 3 -> MaterialTheme.colorScheme.primary
|
||||
else -> LocalContentColor.current.copy(alpha = 0.75f)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,3 +496,12 @@ fun BoardContainer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun daysLeft(targetDate: Long): Long {
|
||||
val today = LocalDate.now()
|
||||
val target = Instant.ofEpochMilli(targetDate)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
|
||||
return ChronoUnit.DAYS.between(today, target)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ import com.flux.data.model.JournalModel
|
||||
import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.ProgressBoardModel
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.Space
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
@@ -122,6 +123,7 @@ import kotlin.math.absoluteValue
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(navController: NavController, states: States, viewModels: ViewModels){
|
||||
val context = LocalContext.current
|
||||
var query by rememberSaveable { mutableStateOf("") }
|
||||
val allSpaces = getSpacesList().filter { it.id!=6 }
|
||||
val lockedWorkspace = states.workspaceState.allWorkspaces.filter { it.passKey?.isNotBlank()==true }.map { it.workspaceId }
|
||||
@@ -139,6 +141,7 @@ fun SearchScreen(navController: NavController, states: States, viewModels: ViewM
|
||||
)
|
||||
)
|
||||
}
|
||||
val notesPreviewMode = states.settings.data.notesPreviewMode
|
||||
val radius = states.settings.data.cornerRadius
|
||||
val is24HourFormat = states.settings.data.is24HourFormat
|
||||
val isMonthlyView = states.settings.data.isCalendarMonthlyView
|
||||
@@ -303,17 +306,23 @@ fun SearchScreen(navController: NavController, states: States, viewModels: ViewM
|
||||
}
|
||||
|
||||
when(selectedSpace.absoluteValue) {
|
||||
1 -> searchedNotes(navController, radius, notes, labels)
|
||||
2 -> searchedTodo(navController, radius, expandedTODOIds.value, todoLists, { id->
|
||||
expandedTODOIds.value =
|
||||
if (id in expandedTODOIds.value) expandedTODOIds.value - id
|
||||
else expandedTODOIds.value + id
|
||||
1 -> searchedNotes(navController, notesPreviewMode, radius, notes, labels)
|
||||
2 -> searchedTodo(navController, radius, context, expandedTODOIds.value, todoLists, { id->
|
||||
val todoItem = todoLists.first { it.id == id }
|
||||
val workspaceId = todoItem.workspaceId
|
||||
|
||||
if(todoItem.recurrence is RecurrenceRule.NONE){
|
||||
expandedTODOIds.value =
|
||||
if (id in expandedTODOIds.value) expandedTODOIds.value - id
|
||||
else expandedTODOIds.value + id
|
||||
}
|
||||
else{ navController.navigate(NavRoutes.TodoDetail.withArgs(workspaceId, id)) }
|
||||
}, viewModels.todoViewModel::onEvent)
|
||||
3 -> searchedEvent(navController, radius, is24HourFormat,pendingTasks, completedTasks, selectedDate, selectedMonth, isMonthlyView, monthlyEventCount, viewModels.eventViewModel::onEvent)
|
||||
4 -> searchedJournal(navController, radius, journals, labels)
|
||||
5 -> searchedHabits(navController, radius, is24HourFormat, currentHabits, pastHabits, states.habitState.allInstances, viewModels.habitViewModel::onEvent)
|
||||
7 -> searchedProgressBoard(radius, notStartedItems, inProgressItems, completedItems) { selectedProgressBoardItem = it }
|
||||
else -> searchedNotes(navController, radius, notes, labels)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,13 +363,14 @@ fun SearchScreen(navController: NavController, states: States, viewModels: ViewM
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun LazyListScope.searchedNotes(navController: NavController, radius: Int, notes: List<NotesModel>, labels: List<LabelModel>){
|
||||
fun LazyListScope.searchedNotes(navController: NavController, notesPreviewMode: Int, radius: Int, notes: List<NotesModel>, labels: List<LabelModel>){
|
||||
if (notes.isEmpty()) item { EmptyNotes() }
|
||||
items(notes, key = { it.notesId }) { note ->
|
||||
NotesPreviewCard(
|
||||
radius = radius,
|
||||
isSelected = false,
|
||||
note = note,
|
||||
notesPreviewMode = notesPreviewMode,
|
||||
labels = labels.filter { note.labels.contains(it.labelId) }.map { it.value },
|
||||
onClick = { navController.navigate(NavRoutes.NoteDetails.withArgs(note.workspaceId, note.notesId)) },
|
||||
onLongPressed = { navController.navigate(NavRoutes.NoteDetails.withArgs(note.workspaceId, note.notesId)) },
|
||||
@@ -571,6 +581,7 @@ fun LazyListScope.searchedProgressBoard(
|
||||
fun LazyListScope.searchedTodo(
|
||||
navController: NavController,
|
||||
radius: Int,
|
||||
context: Context,
|
||||
expandedTODOIds: Set<String>,
|
||||
todoList: List<TodoModel>,
|
||||
onExpandToggle: (String) -> Unit,
|
||||
@@ -582,6 +593,7 @@ fun LazyListScope.searchedTodo(
|
||||
navController = navController,
|
||||
radius = radius,
|
||||
item = todoItem,
|
||||
context = context,
|
||||
workspaceId = todoItem.workspaceId,
|
||||
isExpanded = todoItem.id in expandedTODOIds,
|
||||
onExpandToggle = onExpandToggle,
|
||||
|
||||
@@ -222,35 +222,50 @@ fun Customize(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnRadiusClicked(settings: Settings, onExit: (Int) -> Unit) {
|
||||
val minimalRadius = 5
|
||||
val settingsRadius = settings.data.cornerRadius
|
||||
var sliderPosition by remember { mutableFloatStateOf(((settingsRadius - minimalRadius).toFloat() / 30)) }
|
||||
val realRadius: Int = (((sliderPosition * 100).toInt()) / 3) + minimalRadius
|
||||
fun OnRadiusClicked(
|
||||
settings: Settings,
|
||||
onExit: (Int) -> Unit
|
||||
) {
|
||||
|
||||
var sliderPosition by remember {
|
||||
mutableFloatStateOf(
|
||||
settings.data.cornerRadius
|
||||
.coerceIn(5, 44)
|
||||
.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
val realRadius = sliderPosition.toInt()
|
||||
|
||||
@Composable
|
||||
fun example(shape: RoundedCornerShape) {
|
||||
fun Example(shape: RoundedCornerShape) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp, 3.dp, 32.dp, 1.dp)
|
||||
.padding(horizontal = 32.dp, vertical = 3.dp)
|
||||
.background(
|
||||
shape = shape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = shape
|
||||
)
|
||||
.height(62.dp),
|
||||
.height(62.dp)
|
||||
)
|
||||
}
|
||||
Dialog(onDismissRequest = { onExit(realRadius) }) {
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
onExit(realRadius)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
shape = RoundedCornerShape(realRadius / 3)
|
||||
shape = RoundedCornerShape((realRadius / 3).dp)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize(0.38f)
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.Select_radius),
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -259,14 +274,50 @@ fun OnRadiusClicked(settings: Settings, onExit: (Int) -> Unit) {
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp, bottom = 16.dp)
|
||||
)
|
||||
example(shapeManager(radius = realRadius, isFirst = true))
|
||||
example(shapeManager(radius = realRadius))
|
||||
example(shapeManager(radius = realRadius, isLast = true))
|
||||
|
||||
Example(
|
||||
shapeManager(
|
||||
radius = realRadius,
|
||||
isFirst = true
|
||||
)
|
||||
)
|
||||
|
||||
Example(
|
||||
shapeManager(
|
||||
radius = realRadius
|
||||
)
|
||||
)
|
||||
|
||||
Example(
|
||||
shapeManager(
|
||||
radius = realRadius,
|
||||
isLast = true
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "$realRadius",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
modifier = Modifier.padding(32.dp, 16.dp, 32.dp, 16.dp),
|
||||
colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
onValueChange = { newValue -> sliderPosition = newValue }
|
||||
onValueChange = {
|
||||
sliderPosition = it
|
||||
},
|
||||
valueRange = 5f..44f,
|
||||
steps = 38,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 32.dp,
|
||||
vertical = 16.dp
|
||||
),
|
||||
colors = SliderDefaults.colors(
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.material.icons.automirrored.filled.ChromeReaderMode
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||
import androidx.compose.material.icons.filled.Spellcheck
|
||||
import androidx.compose.material.icons.rounded.FontDownload
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -25,10 +26,12 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.ui.common.BasicScaffold
|
||||
import com.flux.ui.events.SettingEvents
|
||||
import com.flux.ui.state.Settings
|
||||
@@ -90,6 +93,21 @@ fun Editor(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SettingOption(
|
||||
title = stringResource(R.string.notes_preview),
|
||||
description = stringResource(R.string.change_preview_setting_for_notes),
|
||||
icon = Icons.Rounded.FontDownload,
|
||||
radius = shapeManager(
|
||||
radius = settings.data.cornerRadius,
|
||||
isBoth = true
|
||||
),
|
||||
actionType = ActionType.CUSTOM,
|
||||
onCustomClick = { navController.navigate(NavRoutes.NotesPreview.route) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
AnimatedVisibility(visible = !settings.data.dynamicTheme) {
|
||||
@@ -117,15 +135,14 @@ fun Editor(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 0,
|
||||
count = 2
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ChromeReaderMode, null)
|
||||
Text(stringResource(R.string.reading_view))
|
||||
Text(stringResource(R.string.reading_view), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,15 +158,14 @@ fun Editor(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 1,
|
||||
count = 2
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Icon(Icons.Default.EditNote, null)
|
||||
Text(stringResource(R.string.editing_view))
|
||||
Text(stringResource(R.string.editing_view), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.flux.ui.screens.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -11,12 +10,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.ViewCompactAlt
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
@@ -28,6 +23,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
@@ -146,59 +142,41 @@ fun Mode(
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
AnimatedVisibility(visible = !settings.data.dynamicTheme) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 0,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
onSettingsEvents(
|
||||
SettingEvents.UpdateSettings(
|
||||
settings.data.copy(
|
||||
workspaceGridColumns = 1
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 0,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
onSettingsEvents(
|
||||
SettingEvents.UpdateSettings(
|
||||
settings.data.copy(
|
||||
workspaceGridColumns = 1
|
||||
)
|
||||
)
|
||||
},
|
||||
selected = settings.data.workspaceGridColumns==1,
|
||||
label = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.List, null)
|
||||
Text(stringResource(R.string.default_mode))
|
||||
}
|
||||
}
|
||||
)
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 1,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
onSettingsEvents(
|
||||
SettingEvents.UpdateSettings(
|
||||
settings.data.copy(
|
||||
workspaceGridColumns = 2
|
||||
)
|
||||
)
|
||||
},
|
||||
selected = settings.data.workspaceGridColumns == 1,
|
||||
label = { Text(stringResource(R.string.default_mode), maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 1,
|
||||
count = 2
|
||||
),
|
||||
onClick = {
|
||||
onSettingsEvents(
|
||||
SettingEvents.UpdateSettings(
|
||||
settings.data.copy(
|
||||
workspaceGridColumns = 2
|
||||
)
|
||||
)
|
||||
},
|
||||
selected = settings.data.workspaceGridColumns==2,
|
||||
label = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(Icons.Default.ViewCompactAlt, null)
|
||||
Text(stringResource(R.string.select_mode))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
selected = settings.data.workspaceGridColumns == 2,
|
||||
label = { Text(stringResource(R.string.compact_mode), maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package com.flux.ui.screens.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.ui.common.BasicScaffold
|
||||
import com.flux.ui.events.SettingEvents
|
||||
import com.flux.ui.state.Settings
|
||||
|
||||
@Composable
|
||||
fun NotesPreviewSetting(
|
||||
navController: NavController,
|
||||
settings: Settings,
|
||||
onSettingsEvents: (SettingEvents) -> Unit
|
||||
){
|
||||
val radius = settings.data.cornerRadius
|
||||
val notesPreviewMode = settings.data.notesPreviewMode
|
||||
val maxHeight = when (notesPreviewMode) {
|
||||
0 -> 0.dp
|
||||
1 -> 180.dp
|
||||
else -> 360.dp
|
||||
}
|
||||
|
||||
BasicScaffold(
|
||||
title = stringResource(R.string.notes_preview),
|
||||
onBackClicked = { navController.popBackStack() }
|
||||
) { innerPadding ->
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
verticalItemSpacing = 8.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 12.dp,
|
||||
bottom = 140.dp
|
||||
),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)
|
||||
),
|
||||
modifier = Modifier.clip(
|
||||
shapeManager(isBoth = true, radius = radius / 2)
|
||||
),
|
||||
shape = shapeManager(isBoth = true, radius = radius / 2),
|
||||
onClick = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.heightIn(max = if (maxHeight != 0.dp) maxHeight else 100.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.Title),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (maxHeight != 0.dp) {
|
||||
Text(NOTE1, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)
|
||||
),
|
||||
modifier = Modifier.clip(
|
||||
shapeManager(isBoth = true, radius = radius / 2)
|
||||
),
|
||||
shape = shapeManager(isBoth = true, radius = radius / 2),
|
||||
onClick = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.heightIn(max = if (maxHeight != 0.dp) maxHeight else 100.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.Title),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (maxHeight != 0.dp) {
|
||||
Text(NOTE2, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)
|
||||
),
|
||||
modifier = Modifier.clip(
|
||||
shapeManager(isBoth = true, radius = radius / 2)
|
||||
),
|
||||
shape = shapeManager(isBoth = true, radius = radius / 2),
|
||||
onClick = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.heightIn(max = if (maxHeight != 0.dp) maxHeight else 100.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.Title),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (maxHeight != 0.dp) {
|
||||
Text(NOTE3, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)
|
||||
),
|
||||
modifier = Modifier.clip(
|
||||
shapeManager(isBoth = true, radius = radius / 2)
|
||||
),
|
||||
shape = shapeManager(isBoth = true, radius = radius / 2),
|
||||
onClick = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.heightIn(max = if (maxHeight != 0.dp) maxHeight else 100.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.Title),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (maxHeight != 0.dp) {
|
||||
Text(NOTE4, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(12.dp)
|
||||
.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(8.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.change_height),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 3),
|
||||
onClick = { onSettingsEvents(SettingEvents.UpdateSettings(settings.data.copy(notesPreviewMode = 0))) },
|
||||
selected = notesPreviewMode == 0,
|
||||
label = { Text(stringResource(R.string.compact), maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 3),
|
||||
onClick = { onSettingsEvents(SettingEvents.UpdateSettings(settings.data.copy(notesPreviewMode = 1))) },
|
||||
selected = notesPreviewMode == 1,
|
||||
label = { Text(stringResource(R.string.normal), maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(2, 3),
|
||||
onClick = { onSettingsEvents(SettingEvents.UpdateSettings(settings.data.copy(notesPreviewMode = 2))) },
|
||||
selected = notesPreviewMode == 2,
|
||||
label = { Text(stringResource(R.string.elongated), maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val NOTE1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eu sapien sagittis elit tincidunt feugiat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nullam ut nibh eu dolor maximus gravida. Integer non dapibus sem. Nullam nec lectus metus. Nullam vitae fermentum ipsum. Interdum et malesuada fames ac ante ipsum primis in faucibus."
|
||||
const val NOTE2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
||||
const val NOTE3 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eu sapien sagittis elit tincidunt feugiat. Interdum et malesuada fames ac ante ipsum primis in faucibus."
|
||||
const val NOTE4 = "Lorem ipsum dolor sit amet"
|
||||
@@ -465,7 +465,7 @@ fun shapeManager(
|
||||
isFirst: Boolean = false,
|
||||
radius: Int
|
||||
): RoundedCornerShape {
|
||||
val smallerRadius: Dp = (radius / 5).dp
|
||||
val smallerRadius: Dp = (radius / 4).dp
|
||||
val defaultRadius: Dp = radius.dp
|
||||
|
||||
return when {
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
package com.flux.ui.screens.todo
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Alarm
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material.icons.filled.SubdirectoryArrowRight
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.TodoItem
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.ui.common.DeleteAlert
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewTodoList(
|
||||
navController: NavController,
|
||||
is24HoursFormat: Boolean,
|
||||
list: TodoModel,
|
||||
workspaceId: String,
|
||||
onTodoEvents: (TodoEvents) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var title by rememberSaveable { mutableStateOf(list.title) }
|
||||
val itemList = remember { list.items.toMutableStateList() }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var recurrence by remember { mutableStateOf(list.recurrence) }
|
||||
var reminderTime by remember { mutableLongStateOf(list.startDateTime) }
|
||||
val deleteQueue = remember {
|
||||
Channel<Pair<Int, TodoItem>>(Channel.UNLIMITED)
|
||||
}
|
||||
|
||||
// True = normal editing mode (locked ordering)
|
||||
// False = reorder mode (unlocked ordering, drag handles visible)
|
||||
var isReordering by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteDialog) {
|
||||
DeleteAlert(
|
||||
{ showDeleteDialog = false },
|
||||
{
|
||||
onTodoEvents(TodoEvents.DeleteList(context,list))
|
||||
navController.popBackStack()
|
||||
showDeleteDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableState = rememberReorderableLazyListState(
|
||||
lazyListState = lazyListState
|
||||
) { from, to -> itemList.move(from.index, to.index) }
|
||||
|
||||
var isReminderDialogVisible by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val itemRemovedLabel = stringResource(R.string.item_removed)
|
||||
val undoLabel = stringResource(R.string.undo)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
for ((index, removedItem) in deleteQueue) {
|
||||
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = itemRemovedLabel,
|
||||
actionLabel = undoLabel
|
||||
)
|
||||
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
val insertIndex = index.coerceAtMost(itemList.size)
|
||||
itemList.add(insertIndex, removedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.imePadding(),
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
title = {
|
||||
BasicTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
singleLine = true,
|
||||
readOnly = isReordering,
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(capitalization = KeyboardCapitalization.Words),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (title.isBlank()) {
|
||||
Text(
|
||||
text = stringResource(R.string.Title),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton({ navController.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Reminder
|
||||
IconButton(onClick = { isReminderDialogVisible = true }) {
|
||||
Icon(Icons.Default.Alarm, null)
|
||||
}
|
||||
|
||||
// Lock/unlock reorder button
|
||||
IconButton(onClick = { isReordering = !isReordering }) {
|
||||
Icon(if(!isReordering) Icons.Default.Lock else Icons.Default.LockOpen, null)
|
||||
}
|
||||
|
||||
// ✓ in edit mode: persist everything to ViewModel
|
||||
IconButton(
|
||||
enabled = title.isNotBlank(),
|
||||
onClick = {
|
||||
onTodoEvents(
|
||||
TodoEvents.UpsertList(
|
||||
context,
|
||||
list.recurrence is RecurrenceRule.Weekly && recurrence is RecurrenceRule.NONE,
|
||||
list.copy(
|
||||
title = title,
|
||||
items = itemList.toList(),
|
||||
workspaceId = workspaceId,
|
||||
recurrence = recurrence,
|
||||
startDateTime = reminderTime
|
||||
)
|
||||
)
|
||||
)
|
||||
isReordering = false
|
||||
navController.popBackStack()
|
||||
}
|
||||
) { Icon(Icons.Default.Check, null) }
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
itemsIndexed(
|
||||
itemList,
|
||||
key = { _, item -> item.id }
|
||||
) { _, item ->
|
||||
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = item.id
|
||||
) { _ ->
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.animateItem(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
|
||||
// Checkbox: only in editing (locked) mode
|
||||
if (!isReordering) {
|
||||
Checkbox(
|
||||
modifier = Modifier
|
||||
.scale(0.75f)
|
||||
.size(32.dp),
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { checked ->
|
||||
val i = itemList.indexOfFirst { it.id == item.id }
|
||||
if (i >= 0) {
|
||||
itemList[i] = item.copy(isChecked = checked)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = item.value,
|
||||
onValueChange = { newText ->
|
||||
val i = itemList.indexOfFirst { it.id == item.id }
|
||||
if (i >= 0) {
|
||||
itemList[i] = item.copy(value = newText)
|
||||
}
|
||||
},
|
||||
// Editable only in editing mode, read-only during reorder or view
|
||||
readOnly = isReordering,
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.weight(1f),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (item.value.isBlank()) {
|
||||
Text(
|
||||
text = stringResource(R.string.Title),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Drag handle: only in reorder (unlocked) mode
|
||||
if (isReordering) {
|
||||
IconButton(
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.draggableHandle()
|
||||
) {
|
||||
Icon(Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove button: only in editing (locked) mode
|
||||
if (!isReordering) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
val index = itemList.indexOfFirst { it.id == item.id }
|
||||
|
||||
if (index >= 0) {
|
||||
val removedItem = itemList[index]
|
||||
itemList.removeAt(index)
|
||||
scope.launch { deleteQueue.send(index to removedItem) }
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Remove,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add item button: only in editing (locked) mode
|
||||
if (!isReordering) {
|
||||
item {
|
||||
TextButton(
|
||||
onClick = { itemList.add(TodoItem()) },
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.SubdirectoryArrowRight, null)
|
||||
Text(stringResource(R.string.Add_Item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isReminderDialogVisible) {
|
||||
TodoReminderDialog(
|
||||
is24HourFormat = is24HoursFormat,
|
||||
reminderTime = reminderTime,
|
||||
recurrence = recurrence,
|
||||
onDismiss = { isReminderDialogVisible = false }
|
||||
) { newRecurrence, newReminderTime ->
|
||||
recurrence=newRecurrence
|
||||
reminderTime = newReminderTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.move(from: Int, to: Int) {
|
||||
if (from == to) return
|
||||
if (from !in indices) return
|
||||
|
||||
val adjustedTo = when {
|
||||
to < 0 -> 0
|
||||
to > lastIndex -> lastIndex
|
||||
else -> to
|
||||
}
|
||||
|
||||
val item = removeAt(from)
|
||||
add(adjustedTo, item)
|
||||
}
|
||||
@@ -1,71 +1,107 @@
|
||||
package com.flux.ui.screens.todo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.automirrored.outlined.Note
|
||||
import androidx.compose.material.icons.filled.Alarm
|
||||
import androidx.compose.material.icons.filled.AlarmAdd
|
||||
import androidx.compose.material.icons.filled.AlarmOff
|
||||
import androidx.compose.material.icons.filled.AlarmOn
|
||||
import androidx.compose.material.icons.filled.Analytics
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.Repeat
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Circle
|
||||
import androidx.compose.material.icons.outlined.Percent
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoItem
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.isCompleted
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.ui.common.DeleteAlert
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.ui.common.TimePicker
|
||||
import com.flux.ui.common.convertMillisToDate
|
||||
import com.flux.ui.common.convertMillisToTime
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.screens.analytics.HeatMapCard
|
||||
import com.flux.ui.screens.events.toFormattedTime
|
||||
import com.flux.ui.screens.habits.HabitInfoComponent
|
||||
import com.flux.ui.screens.notes.ExportCard
|
||||
import com.flux.ui.screens.settings.shapeManager
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TodoExpandableCard(
|
||||
navController: NavController,
|
||||
radius: Int,
|
||||
context: Context,
|
||||
item: TodoModel,
|
||||
isExpanded: Boolean,
|
||||
workspaceId: String,
|
||||
onExpandToggle: (String) -> Unit,
|
||||
onTodoEvents: (TodoEvents) -> Unit
|
||||
) {
|
||||
var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedItem by remember { mutableStateOf<TodoModel?>(null) }
|
||||
|
||||
if (showDeleteDialog && selectedItem != null) {
|
||||
DeleteAlert(
|
||||
onConfirmation = {
|
||||
onTodoEvents(TodoEvents.DeleteList(selectedItem!!))
|
||||
selectedItem = null
|
||||
showDeleteDialog = false
|
||||
},
|
||||
onDismissRequest = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
shape = if(isExpanded) shapeManager(isBoth = true, radius=radius) else RoundedCornerShape(50),
|
||||
@@ -75,6 +111,7 @@ fun TodoExpandableCard(
|
||||
TodoHeaderRow(
|
||||
id = item.id,
|
||||
title = item.title,
|
||||
isReminderOn = item.recurrence is RecurrenceRule.Weekly,
|
||||
onExpandToggle = onExpandToggle,
|
||||
onNavigate = {
|
||||
navController.navigate(
|
||||
@@ -85,6 +122,7 @@ fun TodoExpandableCard(
|
||||
|
||||
if (isExpanded) {
|
||||
TodoItems(
|
||||
context = context,
|
||||
todoList = item,
|
||||
workspaceId = workspaceId,
|
||||
onTodoEvents = onTodoEvents
|
||||
@@ -98,6 +136,7 @@ fun TodoExpandableCard(
|
||||
private fun TodoHeaderRow(
|
||||
id: String,
|
||||
title: String,
|
||||
isReminderOn: Boolean,
|
||||
onExpandToggle: (String) -> Unit,
|
||||
onNavigate: () -> Unit
|
||||
) {
|
||||
@@ -114,16 +153,27 @@ private fun TodoHeaderRow(
|
||||
fontSize = 16.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f).padding(start = 20.dp, end = 3.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 20.dp, end = 3.dp),
|
||||
)
|
||||
IconButton(onClick = onNavigate) {
|
||||
Icon(Icons.Default.Edit, null)
|
||||
Row {
|
||||
if(isReminderOn){
|
||||
IconButton(onClick = onNavigate) {
|
||||
Icon(Icons.Default.Alarm, null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onNavigate) {
|
||||
Icon(Icons.Default.Analytics, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TodoItems(
|
||||
context: Context,
|
||||
todoList: TodoModel,
|
||||
workspaceId: String,
|
||||
onTodoEvents: (TodoEvents) -> Unit
|
||||
@@ -138,6 +188,8 @@ private fun TodoItems(
|
||||
if (updatedItems != todoList.items) {
|
||||
onTodoEvents(
|
||||
TodoEvents.UpsertList(
|
||||
context,
|
||||
false,
|
||||
todoList.copy(
|
||||
items = updatedItems,
|
||||
workspaceId = workspaceId
|
||||
@@ -151,32 +203,452 @@ private fun TodoItems(
|
||||
val allSortedItems = unCheckedItems + checkedItems
|
||||
|
||||
LazyColumn(
|
||||
Modifier.padding(horizontal = 6.dp).padding(top = 4.dp, bottom = 12.dp).heightIn(max = 400.dp),
|
||||
Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.padding(top = 4.dp, bottom = 12.dp)
|
||||
.heightIn(max = 400.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
items(allSortedItems) { todoItem ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (todoItem.isChecked) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
),
|
||||
onClick = { onToggleCheck(todoItem) }
|
||||
MaterialListItem(true, todoItem){ onToggleCheck(todoItem) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaterialListItem(
|
||||
enabled: Boolean = true,
|
||||
todoItem: TodoItem,
|
||||
onToggleCheck: () -> Unit
|
||||
){
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (todoItem.isChecked) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
),
|
||||
onClick = onToggleCheck
|
||||
) {
|
||||
Row (verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(
|
||||
onToggleCheck, colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = if (todoItem.isChecked) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
contentColor = if (todoItem.isChecked) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Row (verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton({onToggleCheck(todoItem)}, colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = if(todoItem.isChecked) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
contentColor = if(todoItem.isChecked) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||
)) { Icon(if(todoItem.isChecked) Icons.Default.Verified else Icons.Outlined.Circle, null) }
|
||||
Text(
|
||||
text = todoItem.value,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (enabled) { Icon(if (todoItem.isChecked) Icons.Default.Verified else Icons.Outlined.Circle, null) }
|
||||
else { Icon(Icons.Default.Circle, null, tint = MaterialTheme.colorScheme.primary) }
|
||||
}
|
||||
|
||||
Text(
|
||||
text = todoItem.value,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TodoReminderDialog(
|
||||
is24HourFormat: Boolean,
|
||||
reminderTime: Long,
|
||||
recurrence: RecurrenceRule,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (RecurrenceRule, Long) -> Unit
|
||||
){
|
||||
var newReminderTime by remember { mutableLongStateOf(reminderTime) }
|
||||
var currentRecurrence by remember(recurrence) { mutableStateOf(recurrence) }
|
||||
val selectedDays = remember { mutableStateListOf<Int>() }
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(currentRecurrence) {
|
||||
selectedDays.clear()
|
||||
|
||||
if (currentRecurrence is RecurrenceRule.Weekly) {
|
||||
selectedDays.addAll(
|
||||
(currentRecurrence as RecurrenceRule.Weekly).daysOfWeek
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val weekdays = listOf(
|
||||
stringResource(R.string.monday_short),
|
||||
stringResource(R.string.tuesday_short),
|
||||
stringResource(R.string.wednesday_short),
|
||||
stringResource(R.string.thursday_short),
|
||||
stringResource(R.string.friday_short),
|
||||
stringResource(R.string.saturday_short),
|
||||
stringResource(R.string.sunday_short)
|
||||
)
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.padding(8.dp)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.heightIn(min = 180.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Repeat, null)
|
||||
Text(stringResource(R.string.repeat), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Switch(
|
||||
modifier = Modifier.scale(0.8f),
|
||||
checked = currentRecurrence !is RecurrenceRule.NONE, onCheckedChange = {
|
||||
currentRecurrence = if(it) {
|
||||
newReminderTime = if(recurrence is RecurrenceRule.NONE) System.currentTimeMillis()
|
||||
else { reminderTime }
|
||||
|
||||
recurrence as? RecurrenceRule.Weekly ?: RecurrenceRule.Weekly()
|
||||
}
|
||||
else { RecurrenceRule.NONE }
|
||||
})
|
||||
}
|
||||
|
||||
if(currentRecurrence is RecurrenceRule.Weekly){
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
weekdays.forEachIndexed { index, day ->
|
||||
val isSelected = index in selectedDays
|
||||
|
||||
Card(
|
||||
onClick = {
|
||||
if (isSelected) { if (selectedDays.size > 1) { selectedDays.remove(index) } }
|
||||
else { selectedDays.add(index) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.width(56.dp)
|
||||
.height(40.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor =
|
||||
if (isSelected) { MaterialTheme.colorScheme.primary }
|
||||
else { MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp) },
|
||||
contentColor =
|
||||
if (isSelected) { MaterialTheme.colorScheme.onPrimary }
|
||||
else { MaterialTheme.colorScheme.onSurface }
|
||||
)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
){ Text(day) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.AlarmAdd, null)
|
||||
Text(stringResource(R.string.reminder_time), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(newReminderTime.toFormattedTime(is24HourFormat))
|
||||
FilledTonalIconButton({ showTimePicker = true }) {
|
||||
Icon(Icons.Default.Create, null)
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
FilledTonalButton(onDismiss) {
|
||||
Text(stringResource(R.string.Cancel))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
FilledTonalButton({
|
||||
if(currentRecurrence is RecurrenceRule.Weekly){
|
||||
onConfirm(RecurrenceRule.Weekly(selectedDays), newReminderTime)
|
||||
}
|
||||
else { onConfirm(RecurrenceRule.NONE, -1L) }
|
||||
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.Confirm))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text("Turn on the reminder to remind about this to-do list!", modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
FilledTonalButton(onDismiss) {
|
||||
Text(stringResource(R.string.Cancel))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
FilledTonalButton({
|
||||
onConfirm(RecurrenceRule.NONE, reminderTime)
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.Confirm))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showTimePicker) {
|
||||
TimePicker(
|
||||
initialTime = newReminderTime,
|
||||
is24Hour = is24HourFormat,
|
||||
onConfirm = { newReminderTime = it }
|
||||
) { showTimePicker = false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TodoDetailedInfo(
|
||||
radius: Int,
|
||||
list: TodoModel,
|
||||
isReminderOn: Boolean = false,
|
||||
isAllowedToday: Boolean = false,
|
||||
todayInstance: TodoInstance? = null
|
||||
){
|
||||
val items = if(isReminderOn) todayInstance?.items ?: list.items else list.items
|
||||
val completedNumber = items.filter { it.isChecked }.size
|
||||
val remainingNumber = items.filter { !it.isChecked }.size
|
||||
val completedPercentage = (completedNumber * 100f / items.size.coerceAtLeast(1)).toInt()
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = shapeManager(radius = radius * 2),
|
||||
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)),
|
||||
onClick = {}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(Icons.Default.Checklist, null, tint = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = list.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val columns = when {
|
||||
density.fontScale > 1.5f -> 1
|
||||
|
||||
configuration.screenWidthDp < 360 -> 1
|
||||
configuration.screenWidthDp < 480 -> 2
|
||||
|
||||
else -> 3
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(columns),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 1000.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Default.Create,
|
||||
stringResource(R.string.created),
|
||||
convertMillisToDate(list.startDateTime)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
if(isReminderOn) Icons.Default.AlarmOn else Icons.Default.AlarmOff,
|
||||
stringResource(R.string.reminder),
|
||||
if (isReminderOn) stringResource(R.string.on) else stringResource(R.string.off)
|
||||
)
|
||||
}
|
||||
|
||||
if(isReminderOn){
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Default.Alarm,
|
||||
stringResource(R.string.remind_at),
|
||||
convertMillisToTime(list.startDateTime)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Default.DateRange,
|
||||
stringResource(R.string.scheduled),
|
||||
if(isAllowedToday) stringResource(R.string.true_text) else stringResource(R.string.false_text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if((isReminderOn && isAllowedToday) || !isReminderOn){
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Outlined.CheckCircle,
|
||||
stringResource(R.string.completed),
|
||||
completedNumber.toString()
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Outlined.Circle,
|
||||
stringResource(R.string.remaining),
|
||||
remainingNumber.toString()
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
HabitInfoComponent(
|
||||
Icons.Outlined.Percent,
|
||||
stringResource(R.string.completion),
|
||||
"$completedPercentage%"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TodoHeatMap(radius: Int, todoModel: TodoModel, instances: List<TodoInstance>){
|
||||
val today = LocalDate.now()
|
||||
val yearStart = LocalDate.of(today.year, 1, 1)
|
||||
|
||||
// Calculate the offset from Monday for January 1st
|
||||
val jan1DayOfWeek = yearStart.dayOfWeek.value // Monday = 1, Sunday = 7
|
||||
val offsetFromMonday = jan1DayOfWeek - 1 // 0 for Monday, 6 for Sunday
|
||||
|
||||
val totalDays = ChronoUnit.DAYS.between(yearStart, today).toInt() + 1
|
||||
val allDates = (0 until totalDays).map { yearStart.plusDays(it.toLong()) }
|
||||
|
||||
val heatMap = remember(instances, todoModel) {
|
||||
instances
|
||||
.groupBy { LocalDate.ofEpochDay(it.instanceDate) }
|
||||
.mapValues { (_, instancesForDay) ->
|
||||
instancesForDay.count { instance ->
|
||||
instance.isCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create week columns with proper day alignment
|
||||
val weekColumns = mutableListOf<List<LocalDate?>>()
|
||||
var currentWeek = MutableList<LocalDate?>(7) { null }
|
||||
|
||||
// Fill the first week with nulls for days before January 1st
|
||||
for (i in 0 until offsetFromMonday) {
|
||||
currentWeek[i] = null
|
||||
}
|
||||
|
||||
// Add all dates starting from the correct day of week
|
||||
allDates.forEachIndexed { index, date ->
|
||||
val dayIndex = (offsetFromMonday + index) % 7
|
||||
currentWeek[dayIndex] = date
|
||||
|
||||
// When we complete a week (reach Sunday) or it's the last date
|
||||
if (dayIndex == 6 || index == allDates.size - 1) {
|
||||
weekColumns.add(currentWeek.toList())
|
||||
currentWeek = MutableList(7) { null }
|
||||
}
|
||||
}
|
||||
val boxSize = 24.dp
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
// Calculate the index of the current month's first week
|
||||
val currentMonthStartIndex = remember(weekColumns) {
|
||||
val currentMonth = today.month
|
||||
weekColumns.indexOfFirst { week ->
|
||||
week.any { date -> date?.month == currentMonth }
|
||||
}.takeIf { it != -1 } ?: 0
|
||||
}
|
||||
|
||||
// Auto-scroll to current month on first composition
|
||||
LaunchedEffect(currentMonthStartIndex) {
|
||||
if (currentMonthStartIndex > 0) {
|
||||
lazyListState.scrollToItem(index = maxOf(0, currentMonthStartIndex - 2))
|
||||
}
|
||||
}
|
||||
|
||||
HeatMapCard(
|
||||
radius,
|
||||
stringResource(R.string.this_year),
|
||||
"",
|
||||
boxSize,
|
||||
2,
|
||||
lazyListState,
|
||||
weekColumns,
|
||||
heatMap.toMap()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConvertTODODialog(
|
||||
onConfirm: (ConvertType) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.convert),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
ExportCard(Icons.AutoMirrored.Outlined.Note, stringResource(R.string.convert_to_note)) { onConfirm(ConvertType.NOTE) }
|
||||
ExportCard(Icons.Outlined.AutoStories, stringResource(R.string.convert_to_journal)) { onConfirm(ConvertType.JOURNAL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +1,311 @@
|
||||
package com.flux.ui.screens.todo
|
||||
|
||||
import android.app.Activity
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material.icons.filled.SubdirectoryArrowRight
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.TodoItem
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.ui.common.DeleteAlert
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flux.R
|
||||
import com.flux.data.model.JournalModel
|
||||
import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.data.model.toHtml
|
||||
import com.flux.data.model.toMarkdown
|
||||
import com.flux.data.model.toMarkdownContent
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.ConvertType
|
||||
import com.flux.other.DataCopyType
|
||||
import com.flux.other.printPdf
|
||||
import com.flux.other.shareTodo
|
||||
import com.flux.ui.common.DataCopyDialog
|
||||
import com.flux.ui.common.TodoDropdownMenu
|
||||
import com.flux.ui.events.JournalEvents
|
||||
import com.flux.ui.events.NotesEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
import java.time.LocalDate
|
||||
import com.flux.ui.screens.notes.ShareDialog
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TodoDetail(
|
||||
navController: NavController,
|
||||
radius: Int,
|
||||
list: TodoModel,
|
||||
workspaces: List<WorkspaceModel>,
|
||||
instances: List<TodoInstance>,
|
||||
workspaceId: String,
|
||||
onTodoEvents: (TodoEvents) -> Unit
|
||||
onTodoEvents: (TodoEvents) -> Unit,
|
||||
onNotesEvents: (NotesEvents) -> Unit,
|
||||
onJournalEvents: (JournalEvents) -> Unit,
|
||||
onWorkspaceEvents: (WorkspaceEvents) -> Unit
|
||||
) {
|
||||
var title by rememberSaveable { mutableStateOf(list.title) }
|
||||
val itemList = rememberSaveable { list.items.toMutableStateList() }
|
||||
val todayEpoch = LocalDate.now().toEpochDay()
|
||||
val context = LocalContext.current
|
||||
val currentWorkspace = workspaces.find { it.workspaceId == workspaceId }
|
||||
val isReminderOn = list.recurrence is RecurrenceRule.Weekly
|
||||
var showShareDialog by remember { mutableStateOf(false) }
|
||||
var showConvertDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var isEditing by remember { mutableStateOf(list.title.isBlank() && list.items.isEmpty()) }
|
||||
var showDataCopyDialog by remember { mutableStateOf(false) }
|
||||
val todayInstance = instances.find { it.instanceDate == todayEpoch }
|
||||
val cloneString = stringResource(R.string.clone_created_successfully)
|
||||
val contentCopiedString = stringResource(R.string.content_copied)
|
||||
val contentMovedString = stringResource(R.string.content_moved)
|
||||
val successString = stringResource(R.string.success)
|
||||
|
||||
val webView = WebView(context)
|
||||
webView.settings.javaScriptEnabled = false
|
||||
|
||||
webView.loadDataWithBaseURL(
|
||||
null,
|
||||
if(isReminderOn) todayInstance?.toHtml(list.title) ?: list.toHtml() else list.toHtml(),
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null
|
||||
)
|
||||
|
||||
if (showDeleteDialog) {
|
||||
DeleteAlert(
|
||||
{ showDeleteDialog = false },
|
||||
{
|
||||
onTodoEvents(TodoEvents.DeleteList(list))
|
||||
onTodoEvents(TodoEvents.DeleteList(context,list))
|
||||
navController.popBackStack()
|
||||
showDeleteDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun isAllowed(epochDay: Long): Boolean {
|
||||
val localDate = LocalDate.ofEpochDay(epochDay)
|
||||
// Convert to Monday=0, Tuesday=1, ..., Sunday=6 format
|
||||
val dayOfWeek = (localDate.dayOfWeek.value + 6) % 7
|
||||
return dayOfWeek in (list.recurrence as RecurrenceRule.Weekly).daysOfWeek
|
||||
}
|
||||
|
||||
LaunchedEffect(list.id, list.recurrence, todayInstance) {
|
||||
if(isReminderOn && isAllowed(todayEpoch) && todayInstance == null){
|
||||
onTodoEvents(TodoEvents.CreateInstance(list.id, workspaceId))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.imePadding(),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
title = { Text(if (isEditing) stringResource(R.string.Edit_list) else title) },
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
IconButton({ navController.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isEditing) {
|
||||
IconButton(
|
||||
enabled = title.isNotBlank(),
|
||||
onClick = {
|
||||
onTodoEvents(
|
||||
TodoEvents.UpsertList(
|
||||
list.copy(
|
||||
title = title,
|
||||
items = itemList.toList(),
|
||||
workspaceId = workspaceId
|
||||
)
|
||||
)
|
||||
)
|
||||
isEditing = false
|
||||
}
|
||||
) { Icon(Icons.Default.Check, null) }
|
||||
} else {
|
||||
IconButton({ isEditing = true }) { Icon(Icons.Default.Edit, null) }
|
||||
IconButton({ showDeleteDialog = true }) {
|
||||
Icon(
|
||||
Icons.Default.DeleteOutline,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
IconButton({ navController.navigate(NavRoutes.NewTodoList.withArgs(workspaceId, list.id)) }) {
|
||||
Icon(Icons.Default.Edit, null)
|
||||
}
|
||||
|
||||
TodoDropdownMenu(
|
||||
(isReminderOn && isAllowed(todayEpoch) && todayInstance!=null) || !isReminderOn,
|
||||
{ showShareDialog = true },
|
||||
{ printPdf(context as Activity, webView, list.title) },
|
||||
{
|
||||
onTodoEvents(TodoEvents.UpsertList(context, false,TodoModel(items = list.items, recurrence = list.recurrence, workspaceId = list.workspaceId, title = "Clone ${list.title}")))
|
||||
Toast.makeText(context, cloneString, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
{ showDataCopyDialog = true },
|
||||
{ showConvertDialog = true },
|
||||
{ showDeleteDialog = true}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(Modifier.padding(innerPadding)) {
|
||||
if (isEditing) {
|
||||
item {
|
||||
TextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
singleLine = true,
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.Title))
|
||||
},
|
||||
textStyle = MaterialTheme.typography.titleLarge,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(capitalization = KeyboardCapitalization.Words),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
unfocusedIndicatorColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
Modifier.padding(innerPadding).padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if(!isReminderOn){
|
||||
item{ TodoDetailedInfo(radius, list) }
|
||||
|
||||
itemsIndexed(itemList, key = { _, item -> item.id }) { index, item ->
|
||||
if (index >= itemList.size) return@itemsIndexed
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { checked ->
|
||||
if (isEditing) {
|
||||
val i = itemList.indexOfFirst { it.id == item.id }
|
||||
if (i >= 0) itemList[i] = item.copy(isChecked = checked)
|
||||
}
|
||||
items(list.items) { todoItem ->
|
||||
MaterialListItem(true, todoItem){
|
||||
val updatedItems = list.items.map {
|
||||
if (it.id == todoItem.id) it.copy(isChecked = !it.isChecked)
|
||||
else it
|
||||
}
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = item.value,
|
||||
onValueChange = { newText ->
|
||||
val i = itemList.indexOfFirst { it.id == item.id }
|
||||
if (i >= 0) itemList[i] = item.copy(value = newText)
|
||||
},
|
||||
readOnly = !isEditing,
|
||||
singleLine = true,
|
||||
placeholder = { Text(stringResource(R.string.Title)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Done),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
unfocusedIndicatorColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
)
|
||||
|
||||
if (isEditing) {
|
||||
IconButton({
|
||||
val i = itemList.indexOfFirst { it.id == item.id }
|
||||
if (i >= 0) itemList.removeAt(i)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Remove,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
if (updatedItems != list.items) {
|
||||
onTodoEvents(
|
||||
TodoEvents.UpsertList(
|
||||
context,
|
||||
false,
|
||||
list.copy(
|
||||
items = updatedItems,
|
||||
workspaceId = workspaceId
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
item {
|
||||
TextButton(
|
||||
onClick = { itemList.add(TodoItem()) },
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.SubdirectoryArrowRight, null)
|
||||
Text(stringResource(R.string.Add_Item))
|
||||
else if(isAllowed(todayEpoch) && todayInstance!=null) {
|
||||
item{ TodoDetailedInfo(radius, list, isReminderOn = true, isAllowedToday = true, todayInstance) }
|
||||
item{ TodoHeatMap(radius, list, instances) }
|
||||
|
||||
items(todayInstance.items) { todoItem ->
|
||||
MaterialListItem(true, todoItem){
|
||||
val updatedItems = todayInstance.items.map {
|
||||
if (it.id == todoItem.id) it.copy(isChecked = !it.isChecked)
|
||||
else it
|
||||
}
|
||||
|
||||
if (updatedItems != todayInstance.items) {
|
||||
onTodoEvents(TodoEvents.UpsertInstance(todayInstance.copy(items = updatedItems)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if(!isAllowed(todayEpoch)){
|
||||
item{
|
||||
TodoDetailedInfo(radius, list,
|
||||
isReminderOn = true,
|
||||
isAllowedToday = false,
|
||||
todayInstance = todayInstance)
|
||||
}
|
||||
item{ TodoHeatMap(radius, list, instances) }
|
||||
|
||||
items(list.items) { todoItem ->
|
||||
MaterialListItem(false, todoItem){}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showShareDialog) {
|
||||
ShareDialog(true, {
|
||||
shareTodo(
|
||||
context = context,
|
||||
exportType = it,
|
||||
list = list,
|
||||
readWebView = webView
|
||||
)
|
||||
showShareDialog = false
|
||||
}) { showShareDialog = false }
|
||||
}
|
||||
|
||||
if(showDataCopyDialog){
|
||||
DataCopyDialog(
|
||||
workspaces.filterNot { it.workspaceId == workspaceId },
|
||||
{ dataCopyType, selectedWorkspaces ->
|
||||
if(selectedWorkspaces.isEmpty()) return@DataCopyDialog
|
||||
when(dataCopyType){
|
||||
DataCopyType.COPY -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(2)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 2)))
|
||||
}
|
||||
onTodoEvents(TodoEvents.UpsertList(context, false,TodoModel(items = list.items, recurrence = list.recurrence, workspaceId = workspace.workspaceId, title = list.title)))
|
||||
}
|
||||
|
||||
Toast.makeText(context, contentCopiedString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
DataCopyType.MOVE -> {
|
||||
selectedWorkspaces.forEach { workspace ->
|
||||
if(!workspace.selectedSpaces.contains(2)){
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(workspace.copy(selectedSpaces = workspace.selectedSpaces + 2)))
|
||||
}
|
||||
onTodoEvents(TodoEvents.UpsertList(context, false,TodoModel(items = list.items, recurrence = list.recurrence, workspaceId = workspace.workspaceId, title = list.title)))
|
||||
}
|
||||
|
||||
navController.popBackStack()
|
||||
Toast.makeText(context, contentMovedString, Toast.LENGTH_SHORT).show()
|
||||
onTodoEvents(TodoEvents.DeleteList(context, list))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { showDataCopyDialog = false }
|
||||
}
|
||||
|
||||
if(showConvertDialog){
|
||||
ConvertTODODialog ({
|
||||
when(it){
|
||||
ConvertType.NOTE -> {
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(1)){
|
||||
val newSelectedSpaces = currentWorkspace.selectedSpaces + 1
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = newSelectedSpaces)))
|
||||
}
|
||||
|
||||
onNotesEvents(
|
||||
NotesEvents.UpsertNote(
|
||||
NotesModel(
|
||||
title = list.title,
|
||||
description = list.toMarkdownContent(),
|
||||
workspaceId = list.workspaceId
|
||||
)
|
||||
)
|
||||
)
|
||||
navController.popBackStack()
|
||||
onTodoEvents(TodoEvents.DeleteList(context,list))
|
||||
}
|
||||
ConvertType.JOURNAL -> {
|
||||
if(!currentWorkspace!!.selectedSpaces.contains(4)){
|
||||
val newSelectedSpaces = currentWorkspace.selectedSpaces + 4
|
||||
onWorkspaceEvents(WorkspaceEvents.UpsertSpace(currentWorkspace.copy(selectedSpaces = newSelectedSpaces)))
|
||||
}
|
||||
|
||||
onJournalEvents(
|
||||
JournalEvents.UpsertEntry(
|
||||
JournalModel(
|
||||
text = list.toMarkdown(),
|
||||
workspaceId = list.workspaceId
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
navController.popBackStack()
|
||||
onTodoEvents(TodoEvents.DeleteList(context,list))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
Toast.makeText(context, successString, Toast.LENGTH_SHORT).show()
|
||||
showConvertDialog=false
|
||||
}) {
|
||||
showConvertDialog=false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import com.flux.navigation.Loader
|
||||
import com.flux.navigation.NavRoutes
|
||||
@@ -42,6 +44,7 @@ import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.screens.workspaces.SpacesToolBar
|
||||
import com.flux.ui.state.Settings
|
||||
import com.flux.ui.state.TodoState
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -58,6 +61,7 @@ fun TodoScreen(
|
||||
onToggleLock: () -> Unit,
|
||||
onEvent: (TodoEvents) -> Unit
|
||||
){
|
||||
val context = LocalContext.current
|
||||
val workspaceId = workspace.workspaceId
|
||||
val isLoading = state.isLoading
|
||||
val radius = settings.data.cornerRadius
|
||||
@@ -68,7 +72,8 @@ fun TodoScreen(
|
||||
it.title.contains(query, ignoreCase = true) ||
|
||||
it.items.any { item -> item.value.contains(query, ignoreCase = true) }
|
||||
)
|
||||
}
|
||||
}.sortedBy { it.startDateTime }
|
||||
|
||||
var showSearchBar by remember { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
val expandedTODOIds = rememberSaveable(workspaceId) { mutableStateOf<Set<String>>(emptySet()) }
|
||||
@@ -92,7 +97,7 @@ fun TodoScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton({ navController.navigate(NavRoutes.TodoDetail.withArgs(workspaceId, "")) }) {
|
||||
FloatingActionButton({ navController.navigate(NavRoutes.NewTodoList.withArgs(workspaceId, "")) }) {
|
||||
Icon(Icons.Default.AddTask, null)
|
||||
}
|
||||
}
|
||||
@@ -157,12 +162,18 @@ fun TodoScreen(
|
||||
navController = navController,
|
||||
radius = radius,
|
||||
item = todoItem,
|
||||
context = context,
|
||||
workspaceId = workspaceId,
|
||||
isExpanded = todoItem.id in expandedTODOIds.value,
|
||||
onExpandToggle = { id->
|
||||
expandedTODOIds.value =
|
||||
if (id in expandedTODOIds.value) expandedTODOIds.value - id
|
||||
else expandedTODOIds.value + id
|
||||
if(todoItem.recurrence is RecurrenceRule.NONE){
|
||||
expandedTODOIds.value =
|
||||
if (id in expandedTODOIds.value) expandedTODOIds.value - id
|
||||
else expandedTODOIds.value + id
|
||||
}
|
||||
else{
|
||||
navController.navigate(NavRoutes.TodoDetail.withArgs(workspaceId, id))
|
||||
}
|
||||
},
|
||||
onTodoEvents = onEvent
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -27,7 +28,6 @@ import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -412,6 +412,9 @@ fun SpacesToolBar(
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.widthIn(max=100.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
@@ -162,7 +162,7 @@ fun removeSpaceData(
|
||||
) {
|
||||
when (spaceId) {
|
||||
1 -> viewModels.notesViewModel.onEvent(NotesEvents.DeleteAllWorkspaceNotes(workspaceId))
|
||||
2 -> viewModels.todoViewModel.onEvent(TodoEvents.DeleteAllWorkspaceLists(workspaceId))
|
||||
2 -> viewModels.todoViewModel.onEvent(TodoEvents.DeleteAllWorkspaceLists(context, workspaceId))
|
||||
3 -> viewModels.eventViewModel.onEvent(TaskEvents.DeleteAllWorkspaceEvents(workspaceId, context))
|
||||
4 -> viewModels.journalViewModel.onEvent(JournalEvents.DeleteWorkspaceEntries(workspaceId))
|
||||
5 -> viewModels.habitViewModel.onEvent(HabitEvents.DeleteAllWorkspaceHabits(workspaceId, context))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.flux.ui.state
|
||||
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
|
||||
data class TodoState(
|
||||
val isLoading: Boolean = true,
|
||||
val workspaceId: String? = null,
|
||||
val allLists: List<TodoModel> = emptyList()
|
||||
val allLists: List<TodoModel> = emptyList(),
|
||||
val allInstances: List<TodoInstance> = emptyList()
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.flux.data.database.FluxBackup
|
||||
import com.flux.data.database.FluxDatabase
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.SettingsModel
|
||||
import com.flux.data.model.toScheduleRequest
|
||||
import com.flux.data.repository.SettingsRepository
|
||||
@@ -86,6 +87,7 @@ class BackupViewModel @Inject constructor(
|
||||
workspaces = db.workspaceDao.getAll(),
|
||||
notes = db.notesDao.loadAllNotes(),
|
||||
todos = db.todoDao.loadAllLists(),
|
||||
todoInstances = db.todoInstanceDao.loadAllInstances(),
|
||||
habits = db.habitDao.loadAllHabits(),
|
||||
habitInstances = db.habitInstanceDao.loadAllInstances(),
|
||||
journals = db.journalDao.loadAllEntries(),
|
||||
@@ -127,7 +129,15 @@ class BackupViewModel @Inject constructor(
|
||||
|
||||
// --- Todos ---
|
||||
backup.todos.forEach { todo ->
|
||||
if (!db.todoDao.exists(todo.id)) db.todoDao.upsertList(todo)
|
||||
if (!db.todoDao.exists(todo.id)) {
|
||||
if(todo.recurrence== RecurrenceRule.Weekly) scheduleNextReminder(context, todo.toScheduleRequest())
|
||||
db.todoDao.upsertList(todo)
|
||||
}
|
||||
}
|
||||
|
||||
backup.todoInstances.forEach { instance ->
|
||||
if (!db.todoInstanceDao.exists(instance.todoId, instance.instanceDate))
|
||||
db.todoInstanceDao.upsertTodoInstance(instance)
|
||||
}
|
||||
|
||||
// --- Habits ---
|
||||
|
||||
@@ -88,7 +88,7 @@ class JournalViewModel @Inject constructor(
|
||||
YamlFrontMatterExtension.create()
|
||||
)
|
||||
private var parser: Parser = Parser.builder().extensions(extensions).build()
|
||||
private var renderer: HtmlRenderer = HtmlRenderer.builder().extensions(extensions).build()
|
||||
private var renderer: HtmlRenderer = HtmlRenderer.builder().extensions(extensions).softbreak("<br>\n").build()
|
||||
private var lastOutlineContentHash: Int? = null
|
||||
|
||||
fun renderMarkdown(markdown: String): String {
|
||||
|
||||
@@ -79,7 +79,7 @@ class NotesViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
private var parser: Parser = Parser.builder().extensions(extensions).build()
|
||||
private var renderer: HtmlRenderer = HtmlRenderer.builder().extensions(extensions).build()
|
||||
private var renderer: HtmlRenderer = HtmlRenderer.builder().extensions(extensions).softbreak("<br>\n").build()
|
||||
private var lastOutlineContentHash: Int? = null
|
||||
|
||||
fun renderMarkdown(markdown: String): String {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.flux.ui.viewModel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.TodoInstance
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.toScheduleRequest
|
||||
import com.flux.data.repository.TodoRepository
|
||||
import com.flux.other.cancelReminder
|
||||
import com.flux.other.scheduleNextReminder
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.state.TodoState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -12,7 +18,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -33,29 +42,76 @@ class TodoViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
repository.loadTodoData()
|
||||
.collect { lists -> updateState { it.copy(isLoading = false, allLists = lists) } }
|
||||
combine(
|
||||
repository.loadTodoData(),
|
||||
repository.loadAllTodoInstance()
|
||||
) { lists, instances ->
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
allLists = lists,
|
||||
allInstances = instances
|
||||
)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reduce(event: TodoEvents) {
|
||||
when (event) {
|
||||
is TodoEvents.DeleteList -> { deleteList(event.data) }
|
||||
is TodoEvents.UpsertList -> { upsertList(event.data) }
|
||||
is TodoEvents.DeleteAllWorkspaceLists -> deleteWorkspaceLists(event.workspaceId)
|
||||
is TodoEvents.DeleteList -> { deleteList(event.context, event.data) }
|
||||
is TodoEvents.UpsertList -> { upsertList(event.context, event.isRemovingReminder, event.data) }
|
||||
is TodoEvents.DeleteAllWorkspaceLists -> deleteWorkspaceLists(event.context, event.workspaceId)
|
||||
is TodoEvents.CreateInstance -> createInstance(event.listId, event.workspaceId)
|
||||
is TodoEvents.UpsertInstance -> upsertInstance(event.instance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteWorkspaceLists(workspaceId: String) {
|
||||
private fun createInstance(listId: String, workspaceId: String) {
|
||||
val todayEpoch = LocalDate.now().toEpochDay()
|
||||
val items = _state.value.allLists.find { it.id == listId }!!.items.map { it.copy(isChecked = false) }
|
||||
|
||||
val instance = TodoInstance(
|
||||
todoId = listId,
|
||||
workspaceId = workspaceId,
|
||||
instanceDate = todayEpoch,
|
||||
items = items
|
||||
)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (!repository.existInstance(listId, todayEpoch)) {
|
||||
repository.upsertInstance(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertInstance(instance: TodoInstance) {
|
||||
viewModelScope.launch(Dispatchers.IO) { repository.upsertInstance(instance) }
|
||||
}
|
||||
|
||||
private fun deleteWorkspaceLists(context: Context, workspaceId: String) {
|
||||
_state.value.allLists.forEach {data->
|
||||
if(data.recurrence is RecurrenceRule.Weekly) cancelReminder(context,data.toScheduleRequest())
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { repository.deleteAllWorkspaceLists(workspaceId) }
|
||||
}
|
||||
|
||||
private fun deleteList(data: TodoModel) {
|
||||
viewModelScope.launch(Dispatchers.IO) { repository.deleteList(data) }
|
||||
private fun deleteList(context: Context, data: TodoModel) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if(data.recurrence is RecurrenceRule.Weekly) cancelReminder(context,data.toScheduleRequest())
|
||||
repository.deleteList(data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertList(data: TodoModel) {
|
||||
private fun upsertList(context: Context, isRemovingRecurrence: Boolean, data: TodoModel) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if(data.recurrence is RecurrenceRule.Weekly){
|
||||
cancelReminder(context,data.toScheduleRequest())
|
||||
scheduleNextReminder(context, data.toScheduleRequest())
|
||||
}
|
||||
if(isRemovingRecurrence){
|
||||
cancelReminder(context,data.toScheduleRequest())
|
||||
}
|
||||
|
||||
repository.upsertList(data)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -475,5 +475,44 @@
|
||||
<string name="clear">Löschen</string>
|
||||
<string name="spaces">Bereiche</string>
|
||||
<string name="workspaces">Arbeitsbereiche</string>
|
||||
<string name="item_removed">Element entfernt</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
|
||||
<!-- German (de) -->
|
||||
<string name="notes_preview">Notizvorschau</string>
|
||||
<string name="change_preview_setting_for_notes">Vorschaueinstellungen für Notizen ändern</string>
|
||||
<string name="change_height">Höhe ändern</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="elongated">Verlängert</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="compact">Kompakt</string>
|
||||
|
||||
<string name="content_copied">Inhalt kopiert!</string>
|
||||
<string name="content_moved">Inhalt verschoben!</string>
|
||||
<string name="clone_created_successfully">Klon erfolgreich erstellt</string>
|
||||
<string name="days_left">Noch %1$d Tage</string>
|
||||
<string name="one_day_left">Noch 1 Tag</string>
|
||||
<string name="one_day_overdue">1 Tag überfällig</string>
|
||||
<string name="days_overdue">%1$d Tage überfällig</string>
|
||||
<string name="start_date_after_target_error">Das Startdatum darf nicht nach dem Zieldatum liegen</string>
|
||||
<string name="target_date_before_start_error">Das Zieldatum darf nicht vor dem Startdatum liegen</string>
|
||||
<string name="convert">Konvertieren</string>
|
||||
<string name="convert_to_note">In Notiz umwandeln</string>
|
||||
<string name="convert_to_todo">In Aufgabenliste umwandeln</string>
|
||||
<string name="convert_to_journal">In Journal umwandeln</string>
|
||||
<string name="clone">Klonen</string>
|
||||
<string name="copy">Kopieren</string>
|
||||
<string name="move">Verschieben</string>
|
||||
<string name="select_workspaces">Arbeitsbereiche auswählen</string>
|
||||
<string name="turn_on_reminder">Aktiviere die Erinnerung für diese Aufgabenliste!</string>
|
||||
<string name="created">Erstellt</string>
|
||||
<string name="reminder">Erinnerung</string>
|
||||
<string name="scheduled">Geplant</string>
|
||||
<string name="true_text">Wahr</string>
|
||||
<string name="false_text">Falsch</string>
|
||||
<string name="off">Aus</string>
|
||||
<string name="completed">Abgeschlossen</string>
|
||||
<string name="remaining">Verbleibend</string>
|
||||
|
||||
</resources>
|
||||
@@ -481,5 +481,42 @@
|
||||
<string name="clear">Limpiar</string>
|
||||
<string name="spaces">Espacios</string>
|
||||
<string name="workspaces">Espacios de trabajo</string>
|
||||
<string name="item_removed">Elemento eliminado</string>
|
||||
<string name="undo">Deshacer</string>
|
||||
<string name="notes_preview">Vista previa de notas</string>
|
||||
<string name="change_preview_setting_for_notes">Cambiar la configuración de vista previa de las notas</string>
|
||||
<string name="change_height">Cambiar altura</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="elongated">Alargado</string>
|
||||
<string name="video">Vídeo</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="compact">Compacto</string>
|
||||
|
||||
<string name="content_copied">¡Contenido copiado!</string>
|
||||
<string name="content_moved">¡Contenido movido!</string>
|
||||
<string name="clone_created_successfully">Clon creado correctamente</string>
|
||||
<string name="days_left">Quedan %1$d días</string>
|
||||
<string name="one_day_left">Queda 1 día</string>
|
||||
<string name="one_day_overdue">1 día de retraso</string>
|
||||
<string name="days_overdue">%1$d días de retraso</string>
|
||||
<string name="start_date_after_target_error">La fecha de inicio no puede ser posterior a la fecha objetivo</string>
|
||||
<string name="target_date_before_start_error">La fecha objetivo no puede ser anterior a la fecha de inicio</string>
|
||||
<string name="convert">Convertir</string>
|
||||
<string name="convert_to_note">Convertir en nota</string>
|
||||
<string name="convert_to_todo">Convertir en tarea</string>
|
||||
<string name="convert_to_journal">Convertir en diario</string>
|
||||
<string name="clone">Clonar</string>
|
||||
<string name="copy">Copiar</string>
|
||||
<string name="move">Mover</string>
|
||||
<string name="select_workspaces">Seleccionar espacios de trabajo</string>
|
||||
<string name="turn_on_reminder">Activa el recordatorio para esta lista de tareas.</string>
|
||||
<string name="created">Creado</string>
|
||||
<string name="reminder">Recordatorio</string>
|
||||
<string name="scheduled">Programado</string>
|
||||
<string name="true_text">Verdadero</string>
|
||||
<string name="false_text">Falso</string>
|
||||
<string name="off">Desactivado</string>
|
||||
<string name="completed">Completado</string>
|
||||
<string name="remaining">Restante</string>
|
||||
|
||||
</resources>
|
||||
@@ -484,5 +484,42 @@
|
||||
<string name="clear">Effacer</string>
|
||||
<string name="spaces">Espaces</string>
|
||||
<string name="workspaces">Espaces de travail</string>
|
||||
<string name="item_removed">Élément supprimé</string>
|
||||
<string name="undo">Annuler</string>
|
||||
<string name="notes_preview">Aperçu des notes</string>
|
||||
<string name="change_preview_setting_for_notes">Modifier les paramètres d’aperçu des notes</string>
|
||||
<string name="change_height">Modifier la hauteur</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="elongated">Allongé</string>
|
||||
<string name="video">Vidéo</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="compact">Compact</string>
|
||||
|
||||
<string name="content_copied">Contenu copié !</string>
|
||||
<string name="content_moved">Contenu déplacé !</string>
|
||||
<string name="clone_created_successfully">Clone créé avec succès</string>
|
||||
<string name="days_left">%1$d jours restants</string>
|
||||
<string name="one_day_left">1 jour restant</string>
|
||||
<string name="one_day_overdue">1 jour de retard</string>
|
||||
<string name="days_overdue">%1$d jours de retard</string>
|
||||
<string name="start_date_after_target_error">La date de début ne peut pas être après la date cible</string>
|
||||
<string name="target_date_before_start_error">La date cible ne peut pas être avant la date de début</string>
|
||||
<string name="convert">Convertir</string>
|
||||
<string name="convert_to_note">Convertir en note</string>
|
||||
<string name="convert_to_todo">Convertir en tâche</string>
|
||||
<string name="convert_to_journal">Convertir en journal</string>
|
||||
<string name="clone">Cloner</string>
|
||||
<string name="copy">Copier</string>
|
||||
<string name="move">Déplacer</string>
|
||||
<string name="select_workspaces">Sélectionner des espaces de travail</string>
|
||||
<string name="turn_on_reminder">Activez le rappel pour cette liste de tâches !</string>
|
||||
<string name="created">Créé</string>
|
||||
<string name="reminder">Rappel</string>
|
||||
<string name="scheduled">Planifié</string>
|
||||
<string name="true_text">Vrai</string>
|
||||
<string name="false_text">Faux</string>
|
||||
<string name="off">Désactivé</string>
|
||||
<string name="completed">Terminé</string>
|
||||
<string name="remaining">Restant</string>
|
||||
|
||||
</resources>
|
||||
@@ -483,4 +483,41 @@
|
||||
<string name="clear">साफ़ करें</string>
|
||||
<string name="spaces">स्पेस</string>
|
||||
<string name="workspaces">वर्कस्पेस</string>
|
||||
<string name="item_removed">आइटम हटा दिया गया</string>
|
||||
<string name="undo">पूर्ववत करें</string>
|
||||
<string name="notes_preview">नोट्स पूर्वावलोकन</string>
|
||||
<string name="change_preview_setting_for_notes">नोट्स के पूर्वावलोकन की सेटिंग बदलें</string>
|
||||
<string name="change_height">ऊंचाई बदलें</string>
|
||||
<string name="normal">सामान्य</string>
|
||||
<string name="elongated">लंबा</string>
|
||||
<string name="video">वीडियो</string>
|
||||
<string name="audio">ऑडियो</string>
|
||||
<string name="compact">कॉम्पैक्ट</string>
|
||||
|
||||
<string name="content_copied">सामग्री कॉपी की गई!</string>
|
||||
<string name="content_moved">सामग्री स्थानांतरित की गई!</string>
|
||||
<string name="clone_created_successfully">क्लोन सफलतापूर्वक बनाया गया</string>
|
||||
<string name="days_left">%1$d दिन शेष</string>
|
||||
<string name="one_day_left">1 दिन शेष</string>
|
||||
<string name="one_day_overdue">1 दिन विलंबित</string>
|
||||
<string name="days_overdue">%1$d दिन विलंबित</string>
|
||||
<string name="start_date_after_target_error">प्रारंभ तिथि लक्ष्य तिथि के बाद नहीं हो सकती</string>
|
||||
<string name="target_date_before_start_error">लक्ष्य तिथि प्रारंभ तिथि से पहले नहीं हो सकती</string>
|
||||
<string name="convert">परिवर्तित करें</string>
|
||||
<string name="convert_to_note">नोट में बदलें</string>
|
||||
<string name="convert_to_todo">टू-डू में बदलें</string>
|
||||
<string name="convert_to_journal">जर्नल में बदलें</string>
|
||||
<string name="clone">क्लोन</string>
|
||||
<string name="copy">कॉपी</string>
|
||||
<string name="move">स्थानांतरित करें</string>
|
||||
<string name="select_workspaces">वर्कस्पेस चुनें</string>
|
||||
<string name="turn_on_reminder">इस टू-डू सूची के लिए रिमाइंडर चालू करें!</string>
|
||||
<string name="created">बनाया गया</string>
|
||||
<string name="reminder">रिमाइंडर</string>
|
||||
<string name="scheduled">निर्धारित</string>
|
||||
<string name="true_text">सही</string>
|
||||
<string name="false_text">गलत</string>
|
||||
<string name="off">बंद</string>
|
||||
<string name="completed">पूर्ण</string>
|
||||
<string name="remaining">शेष</string>
|
||||
</resources>
|
||||
|
||||
@@ -483,4 +483,42 @@
|
||||
<string name="clear">Wissen</string>
|
||||
<string name="spaces">Ruimtes</string>
|
||||
<string name="workspaces">Werkruimtes</string>
|
||||
<string name="item_removed">Item verwijderd</string>
|
||||
<string name="undo">Ongedaan maken</string>
|
||||
|
||||
<string name="notes_preview">Notitievoorbeeld</string>
|
||||
<string name="change_preview_setting_for_notes">Voorbeeldinstellingen voor notities wijzigen</string>
|
||||
<string name="change_height">Hoogte wijzigen</string>
|
||||
<string name="normal">Normaal</string>
|
||||
<string name="elongated">Verlengd</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="compact">Compact</string>
|
||||
|
||||
<string name="content_copied">Inhoud gekopieerd!</string>
|
||||
<string name="content_moved">Inhoud verplaatst!</string>
|
||||
<string name="clone_created_successfully">Kloon succesvol aangemaakt</string>
|
||||
<string name="days_left">Nog %1$d dagen</string>
|
||||
<string name="one_day_left">Nog 1 dag</string>
|
||||
<string name="one_day_overdue">1 dag te laat</string>
|
||||
<string name="days_overdue">%1$d dagen te laat</string>
|
||||
<string name="start_date_after_target_error">De startdatum mag niet na de einddatum liggen</string>
|
||||
<string name="target_date_before_start_error">De einddatum mag niet vóór de startdatum liggen</string>
|
||||
<string name="convert">Converteren</string>
|
||||
<string name="convert_to_note">Omzetten naar notitie</string>
|
||||
<string name="convert_to_todo">Omzetten naar takenlijst</string>
|
||||
<string name="convert_to_journal">Omzetten naar dagboek</string>
|
||||
<string name="clone">Klonen</string>
|
||||
<string name="copy">Kopiëren</string>
|
||||
<string name="move">Verplaatsen</string>
|
||||
<string name="select_workspaces">Werkruimten selecteren</string>
|
||||
<string name="turn_on_reminder">Schakel de herinnering in voor deze takenlijst!</string>
|
||||
<string name="created">Aangemaakt</string>
|
||||
<string name="reminder">Herinnering</string>
|
||||
<string name="scheduled">Gepland</string>
|
||||
<string name="true_text">Waar</string>
|
||||
<string name="false_text">Onwaar</string>
|
||||
<string name="off">Uit</string>
|
||||
<string name="completed">Voltooid</string>
|
||||
<string name="remaining">Resterend</string>
|
||||
</resources>
|
||||
|
||||
@@ -483,4 +483,42 @@
|
||||
<string name="clear">Limpar</string>
|
||||
<string name="spaces">Espaços</string>
|
||||
<string name="workspaces">Espaços de trabalho</string>
|
||||
<string name="item_removed">Item removido</string>
|
||||
<string name="undo">Desfazer</string>
|
||||
|
||||
<string name="notes_preview">Pré-visualização de notas</string>
|
||||
<string name="change_preview_setting_for_notes">Alterar as configurações de pré-visualização das notas</string>
|
||||
<string name="change_height">Alterar altura</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="elongated">Alongado</string>
|
||||
<string name="video">Vídeo</string>
|
||||
<string name="audio">Áudio</string>
|
||||
<string name="compact">Compacto</string>
|
||||
|
||||
<string name="content_copied">Conteúdo copiado!</string>
|
||||
<string name="content_moved">Conteúdo movido!</string>
|
||||
<string name="clone_created_successfully">Clone criado com sucesso</string>
|
||||
<string name="days_left">Faltam %1$d dias</string>
|
||||
<string name="one_day_left">Falta 1 dia</string>
|
||||
<string name="one_day_overdue">1 dia de atraso</string>
|
||||
<string name="days_overdue">%1$d dias de atraso</string>
|
||||
<string name="start_date_after_target_error">A data de início não pode ser posterior à data final</string>
|
||||
<string name="target_date_before_start_error">A data final não pode ser anterior à data de início</string>
|
||||
<string name="convert">Converter</string>
|
||||
<string name="convert_to_note">Converter para nota</string>
|
||||
<string name="convert_to_todo">Converter para tarefa</string>
|
||||
<string name="convert_to_journal">Converter para diário</string>
|
||||
<string name="clone">Clonar</string>
|
||||
<string name="copy">Copiar</string>
|
||||
<string name="move">Mover</string>
|
||||
<string name="select_workspaces">Selecionar espaços de trabalho</string>
|
||||
<string name="turn_on_reminder">Ative o lembrete para esta lista de tarefas!</string>
|
||||
<string name="created">Criado</string>
|
||||
<string name="reminder">Lembrete</string>
|
||||
<string name="scheduled">Agendado</string>
|
||||
<string name="true_text">Verdadeiro</string>
|
||||
<string name="false_text">Falso</string>
|
||||
<string name="off">Desligado</string>
|
||||
<string name="completed">Concluído</string>
|
||||
<string name="remaining">Restante</string>
|
||||
</resources>
|
||||
@@ -483,4 +483,43 @@
|
||||
<string name="clear">Очистить</string>
|
||||
<string name="spaces">Пространства</string>
|
||||
<string name="workspaces">Рабочие пространства</string>
|
||||
<string name="item_removed">Элемент удалён</string>
|
||||
<string name="undo">Отменить</string>
|
||||
|
||||
<string name="notes_preview">Предпросмотр заметок</string>
|
||||
<string name="change_preview_setting_for_notes">Изменить настройки предпросмотра заметок</string>
|
||||
<string name="change_height">Изменить высоту</string>
|
||||
<string name="normal">Обычный</string>
|
||||
<string name="elongated">Удлинённый</string>
|
||||
<string name="video">Видео</string>
|
||||
<string name="audio">Аудио</string>
|
||||
<string name="compact">Компактный</string>
|
||||
\
|
||||
<string name="content_copied">Содержимое скопировано!</string>
|
||||
<string name="content_moved">Содержимое перемещено!</string>
|
||||
<string name="clone_created_successfully">Клон успешно создан</string>
|
||||
<string name="days_left">Осталось %1$d дней</string>
|
||||
<string name="one_day_left">Остался 1 день</string>
|
||||
<string name="one_day_overdue">Просрочка 1 день</string>
|
||||
<string name="days_overdue">Просрочка %1$d дней</string>
|
||||
<string name="start_date_after_target_error">Дата начала не может быть позже целевой даты</string>
|
||||
<string name="target_date_before_start_error">Целевая дата не может быть раньше даты начала</string>
|
||||
<string name="convert">Преобразовать</string>
|
||||
<string name="convert_to_note">Преобразовать в заметку</string>
|
||||
<string name="convert_to_todo">Преобразовать в задачу</string>
|
||||
<string name="convert_to_journal">Преобразовать в дневник</string>
|
||||
<string name="clone">Клонировать</string>
|
||||
<string name="copy">Копировать</string>
|
||||
<string name="move">Переместить</string>
|
||||
<string name="select_workspaces">Выбрать рабочие пространства</string>
|
||||
<string name="turn_on_reminder">Включите напоминание для этого списка задач!</string>
|
||||
<string name="created">Создано</string>
|
||||
<string name="reminder">Напоминание</string>
|
||||
<string name="scheduled">Запланировано</string>
|
||||
<string name="true_text">Истина</string>
|
||||
<string name="false_text">Ложь</string>
|
||||
<string name="off">Выкл</string>
|
||||
<string name="completed">Завершено</string>
|
||||
<string name="remaining">Осталось</string>
|
||||
|
||||
</resources>
|
||||
@@ -489,4 +489,41 @@
|
||||
<string name="clear">清除</string>
|
||||
<string name="spaces">空间</string>
|
||||
<string name="workspaces">工作区</string>
|
||||
<string name="item_removed">项目已删除</string>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="notes_preview">笔记预览</string>
|
||||
<string name="change_preview_setting_for_notes">更改笔记预览设置</string>
|
||||
<string name="change_height">更改高度</string>
|
||||
<string name="normal">普通</string>
|
||||
<string name="elongated">加长</string>
|
||||
<string name="video">视频</string>
|
||||
<string name="audio">音频</string>
|
||||
<string name="compact">紧凑</string>
|
||||
|
||||
<string name="content_copied">内容已复制!</string>
|
||||
<string name="content_moved">内容已移动!</string>
|
||||
<string name="clone_created_successfully">克隆创建成功</string>
|
||||
<string name="days_left">剩余 %1$d 天</string>
|
||||
<string name="one_day_left">剩余 1 天</string>
|
||||
<string name="one_day_overdue">逾期 1 天</string>
|
||||
<string name="days_overdue">逾期 %1$d 天</string>
|
||||
<string name="start_date_after_target_error">开始日期不能晚于目标日期</string>
|
||||
<string name="target_date_before_start_error">目标日期不能早于开始日期</string>
|
||||
<string name="convert">转换</string>
|
||||
<string name="convert_to_note">转换为笔记</string>
|
||||
<string name="convert_to_todo">转换为待办事项</string>
|
||||
<string name="convert_to_journal">转换为日记</string>
|
||||
<string name="clone">克隆</string>
|
||||
<string name="copy">复制</string>
|
||||
<string name="move">移动</string>
|
||||
<string name="select_workspaces">选择工作区</string>
|
||||
<string name="turn_on_reminder">为此待办事项列表开启提醒!</string>
|
||||
<string name="created">已创建</string>
|
||||
<string name="reminder">提醒</string>
|
||||
<string name="scheduled">已计划</string>
|
||||
<string name="true_text">真</string>
|
||||
<string name="false_text">假</string>
|
||||
<string name="off">关</string>
|
||||
<string name="completed">已完成</string>
|
||||
<string name="remaining">剩余</string>
|
||||
</resources>
|
||||
@@ -481,5 +481,43 @@
|
||||
<string name="clear">Clear</string>
|
||||
<string name="spaces">Spaces</string>
|
||||
<string name="workspaces">Workspaces</string>
|
||||
<string name="item_removed">Item removed</string>
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
<string name="notes_preview">Notes Preview</string>
|
||||
<string name="change_preview_setting_for_notes">Change Preview Setting for Notes</string>
|
||||
<string name="change_height">Change Height</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="elongated">Elongated</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="compact">Compact</string>
|
||||
|
||||
<string name="content_copied">Content Copied!</string>
|
||||
<string name="content_moved">Content Moved!</string>
|
||||
<string name="clone_created_successfully">Clone created successfully</string>
|
||||
<string name="days_left">%1$d days left</string>
|
||||
<string name="one_day_left">1 day left</string>
|
||||
<string name="one_day_overdue">1 day overdue</string>
|
||||
<string name="days_overdue">%1$d days overdue</string>
|
||||
<string name="start_date_after_target_error">Start date cannot be after target date</string>
|
||||
<string name="target_date_before_start_error">Target date cannot be before start date</string>
|
||||
<string name="convert">Convert</string>
|
||||
<string name="convert_to_note">Convert to Note</string>
|
||||
<string name="convert_to_todo">Convert to Todo</string>
|
||||
<string name="convert_to_journal">Convert to Journal</string>
|
||||
<string name="clone">Clone</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="select_workspaces">Select Workspaces</string>
|
||||
<string name="turn_on_reminder">Turn on the reminder to remind about this to-do list!</string>
|
||||
<string name="created">Created</string>
|
||||
<string name="reminder">Reminder</string>
|
||||
<string name="scheduled">Scheduled</string>
|
||||
<string name="true_text">True</string>
|
||||
<string name="false_text">False</string>
|
||||
<string name="off">Off</string>
|
||||
<string name="completed">Completed</string>
|
||||
<string name="remaining">Remaining</string>
|
||||
|
||||
</resources>
|
||||
+18
-15
@@ -1,33 +1,34 @@
|
||||
[versions]
|
||||
agp = "9.0.1"
|
||||
agp = "9.2.1"
|
||||
flexmarkHtml2mdConverter = "0.64.8"
|
||||
kotlin = "2.3.20"
|
||||
ksp = "2.3.6"
|
||||
kotlinxSerializationJson = "1.10.0"
|
||||
biometric = "1.4.0-alpha05"
|
||||
gson = "2.13.2"
|
||||
coreKtx = "1.18.0"
|
||||
kotlin = "2.4.0"
|
||||
ksp = "2.3.9"
|
||||
kotlinxSerializationJson = "1.11.0"
|
||||
biometric = "1.4.0-alpha07"
|
||||
gson = "2.14.0"
|
||||
coreKtx = "1.19.0"
|
||||
appcompat = "1.7.1"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
lifecycleRuntimeKtx = "2.11.0"
|
||||
activityCompose = "1.13.0"
|
||||
composeBom = "2026.03.00"
|
||||
composeBom = "2026.06.00"
|
||||
coreSplashscreen = "1.2.0"
|
||||
navigationCompose = "2.9.7"
|
||||
navigationCompose = "2.9.8"
|
||||
reorderable = "3.1.0"
|
||||
room = "2.8.4"
|
||||
coilCompose = "2.7.0"
|
||||
|
||||
# Common Mark
|
||||
commonmark = "0.27.1"
|
||||
commonmark = "0.29.0"
|
||||
|
||||
# Hilt
|
||||
hilt = "2.59.2"
|
||||
hiltNavigationCompose = "1.3.0"
|
||||
browser = "1.9.0"
|
||||
browser = "1.10.0"
|
||||
documentfile = "1.1.0"
|
||||
workRuntimeKtx = "2.11.1"
|
||||
foundationLayout = "1.10.5"
|
||||
workRuntimeKtx = "2.11.2"
|
||||
foundationLayout = "1.11.3"
|
||||
adaptive = "1.2.0"
|
||||
foundation = "1.11.0"
|
||||
foundation = "1.11.3"
|
||||
navigationCommonKtx = "2.9.8"
|
||||
|
||||
[libraries]
|
||||
@@ -56,6 +57,7 @@ hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", ve
|
||||
|
||||
#Coil (Image rendering)
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
||||
kotlinMetadataWorkaround = { group = "org.jetbrains.kotlin", name = "kotlin-metadata-jvm", version.ref = "kotlin" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
|
||||
# Commonmark for markdown
|
||||
@@ -76,6 +78,7 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na
|
||||
androidx-compose-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
|
||||
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
|
||||
androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "navigationCommonKtx" }
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
feat:
|
||||
1. Added Undo in Todo Item removal
|
||||
2. Draggable Items to reorder in todo.
|
||||
3. Reminder in Todo Items to remind and analyse the list.
|
||||
fix:
|
||||
1. Dated Journal entry bug
|
||||
2. Create Button Text overflow in note/journal.
|
||||
@@ -0,0 +1,7 @@
|
||||
feat:
|
||||
1. Notes Preview Mode to adjust notes height
|
||||
src:
|
||||
1. Improved Markdown Render in preview mode for media, links and codeblocks
|
||||
fix:
|
||||
1. Progress Tracker date bug.
|
||||
2. Automatically detect line break in editor
|
||||
@@ -0,0 +1,9 @@
|
||||
feat:
|
||||
1. Content copy/move to another workspaces
|
||||
2. Various export options in todo
|
||||
3. Clone a data point
|
||||
4. Responsive UI for various display size.
|
||||
5. Day addition in journal timeline with 24-hour format support
|
||||
|
||||
fix:
|
||||
1. text overflow at various places
|
||||
@@ -11,6 +11,7 @@ pluginManagement {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
|
||||
Reference in New Issue
Block a user