7 Commits
Author SHA1 Message Date
chindaronit 2c85247dd1 fix: Room upgraded to version 5 2026-02-24 20:06:18 +05:30
chindaronit 8f02da5c35 feat: Audio Recorder added for notes and journals.
src: Added multilingual support for newly added strings
docs: changes in changelogs for version 6, new screenshots added.
2026-02-22 21:33:05 +05:30
chindaronit 3b2e433757 src: Updated Guide.md
fix: Updated correct link to privacy policy and guide in About.kt
2026-02-20 22:22:47 +05:30
chindaronit ce18b47cbd src: Added PRIVACY_POLICY.md and Guide.md
fix: NoteScreen Layout fix in pinned and unpinned notes.
2026-02-20 21:57:58 +05:30
chindaronit 9eb2a55350 Merge remote-tracking branch 'origin/master' 2026-02-18 22:48:33 +05:30
chindaronit 56d8f37ab7 feat!: Added automatic BackupManger.kt 2026-02-18 22:48:04 +05:30
Ronit ChindaandGitHub 2106ad1ddc Update README.md 2026-02-17 10:36:01 +05:30
48 changed files with 1738 additions and 220 deletions
+209
View File
@@ -0,0 +1,209 @@
# Index
- [What is Markdown?](#what-is-markdown)
- [What is CommonMark?](#what-is-commonmark)
- [What is GitHub Flavored Markdown?](#what-is-github-flavored-markdown)
- [What is LaTeX?](#what-is-latex)
- [How to use LaTeX in Markdown?](#how-to-use-latex-in-markdown)
- [What is Flux?](#what-is-flux-approach)
- [What has Flux offered?](#what-has-flux-offered)
- [Some Basic Markdown Syntax](#some-basic-markdown-syntax)
- [Some Extended Markdown Syntax](#some-extended-markdown-syntax)
- [LaTeX Math Syntax](#latex-math-syntax)
- [Greek Letters](#greek-letters)
- [References](#references)
# What is Markdown?
Markdown is a plain text format for writing structured documents, based on conventions for
indicating formatting in email and usenet posts.
It was developed by John Gruber (with help from Aaron Swartz) and released in 2004 in the form of a
syntax description and a Perl script (Markdown.pl) for converting Markdown to HTML.
In the next decade, dozens of implementations were developed in many languages.
Some extended the original Markdown syntax with conventions for footnotes, tables, and other
document elements.
Some allowed Markdown documents to be rendered in formats other than HTML.
Websites like Reddit, StackOverflow, and GitHub had millions of people using Markdown.
And Markdown started to be used beyond the web, to author books, articles, slide shows, letters, and
lecture notes.
# What is CommonMark?
CommonMark was developed to address inconsistencies and ambiguities in Markdown.
The CommonMark specification defines rules for elements such as headings, lists, links, emphasis,
and code blocks, among others.
By adhering to the CommonMark standard, developers can ensure consistent rendering of Markdown
content across different applications and platforms.
# What is GitHub Flavored Markdown?
GitHub Flavored Markdown, often shortened as GFM, is the dialect of Markdown that is currently
supported for user content on GitHub.com and GitHub Enterprise.
This formal specification, based on the CommonMark Spec, defines the syntax and semantics of this
dialect.
GFM is a strict superset of CommonMark. All the features which are supported in GitHub user content
and that are not specified on the original CommonMark Spec are hence known as extensions, and
highlighted as such.
While GFM supports a wide range of inputs, its worth noting that GitHub.com and GitHub Enterprise
perform additional post-processing and sanitization after GFM is converted to HTML to ensure
security and consistency of the website.
# What is LaTeX?
LaTeX is a typesetting system commonly used for producing scientific and mathematical documents.
LaTeX provides a powerful way to represent mathematical notation in documents, allowing users to
create complex equations and formulas with ease.
# How to use LaTeX in Markdown?
Mathematical expressions are key to information sharing amongst engineers, scientists, data
scientists, and mathematicians.
You can use the $ and $$ delimiters in GFM to insert math expressions in TeX and LaTeX style syntax.
# What is Flux Approach
Flux is built around the idea that information should be organized based on context, intent, and workflow rather than being confined to a single linear note system.
At the core of this approach is the concept of **Workspaces**.
A workspace acts as an independent environment that users can create based on specific needs or domains of their life or work. For example, a user might create separate workspaces for Personal Life, Work Projects, Research, Study, or Fitness Tracking.
Each workspace functions as a self-contained organizational unit, allowing users to keep related data structured and isolated without interference from other contexts.
Within every workspace, Flux provides multiple **Templates** designed for different types of information and productivity workflows. These templates include, but are not limited to:
* **Notes** — Rich Markdown documents with media and LaTeX support
* **Todo** — Task tracking with completion states and prioritization
* **Events** — Scheduled items with time and reminder support
* **Calendar** — Temporal visualization of events and plans
* **Journal** — Chronological personal or professional entries
* **Analytics** — Insights and productivity metrics derived from workspace data
This modular structure allows users to mix and match templates depending on their requirements. A research workspace might emphasize notes and analytics, while a personal workspace may focus on journals, todos, and events.
By separating data into workspaces and structuring content through templates, Flux enables scalable organization, reduces cognitive clutter, and supports diverse productivity systems within a single unified platform.
# What has Flux offered?
Flux supports CommonMark and GitHub Flavored Markdown (GFM) syntax, as well as LaTeX math
syntax.
This allows users to create richly formatted notes with support for headings, lists, links,
emphasis, code blocks, tables, and mathematical expressions, and allows notes to be exported in
various formats including TXT, MD (Markdown), and HTML.
# Some Basic Markdown Syntax
| Element | Syntax |
|:--------------:|:----------------------------------:|
| Heading | `# H1` <br/> `## H2`<br/> `### H3` |
| Italic | `_italic_ or *italic*` |
| Bold | `**bold** or __bold__` |
| Blockquote | `> Blockquote` |
| Link | `[title](https://www.example.com)` |
| Code | `` `code` `` |
| Ordered List | `1. List item 1 or 2) List item 2` |
| Unordered List | `- Apple or + Banana or * Orange` |
| Image | `![alt text](image.jpeg)` |
# Some Extended Markdown Syntax
| Element | Syntax |
|:-------------:|:----------------------------------:|
| Strikethrough | `~~Strikethrough~~` |
| Underline | `++Underline++` |
| Highlight | `==Marking==` |
| Task List | `- [x] Task 1`<br/> `- [ ] Task 2` |
| Heading ID | `# Heading {#custom-id}` |
# LaTeX Math Syntax
| Element | Syntax |
|:-----------:|:------------------:|
| Inline | `$x^2$` |
| Display | `$$x^2$$` |
| newline | `\\ or \\\\` |
| Whitespace | `\quad or \` |
| Superscript | `x^2` |
| Subscript | `y_1` |
| Expression | `{x}` |
| Overline | `\overline{x}` |
| Underline | `\underline{x}` |
| Fraction | `\frac{x}{y}` |
| Left Paren | `\left(` |
| Right Paren | `\right)` |
| Parentheses | `\left(x+y\right)` |
| Brackets | `\left[x+y\right]` |
| Braces | `\left{x+y\right}` |
| Root | `\sqrt[n]{x}` |
| × | `\times` |
| ÷ | `\div` |
| ± | `\pm` |
| ≠ | `\neq` |
| ≈ | `\approx` |
| ≤ | `\leq` |
| ≥ | `\geq` |
| ∞ | `\infty` |
| ∑ | `\sum` |
| ∏ | `\prod` |
| ∫ | `\int` |
| ∑ | `\sum` |
| lim | `\lim` |
| ∀ | `\forall` |
| ∃ | `\exists` |
| ∴ | `\therefore` |
| ∵ | `\because` |
| ⊂ | `\subset` |
| ⊃ | `\supset` |
| ⊆ | `\subseteq` |
| ⊇ | `\supseteq` |
| ∈ | `\in` |
| ∉ | `\notin` |
# Greek Letters
| Uppercase | Syntax | Lowercase | Syntax |
|:---------:|:----------:|:---------:|:----------:|
| A | `A` | α | `\alpha` |
| Β | `B` | β | `\beta` |
| Γ | `\Gamma` | γ | `\gamma` |
| Δ | `\Delta` | δ | `\delta` |
| Ε | `E` | ε | `\epsilon` |
| Ζ | `Z` | ζ | `\zeta` |
| Η | `H` | η | `\eta` |
| Θ | `\Theta` | θ | `\theta` |
| Ι | `I` | ι | `\iota` |
| Κ | `K` | κ | `\kappa` |
| Λ | `\Lambda` | λ | `\lambda` |
| Μ | `M` | μ | `\mu` |
| Ν | `N` | ν | `\nu` |
| Ξ | `\Xi` | ξ | `\xi` |
| Ο | `O` | ο | `\omicron` |
| Π | `\Pi` | π | `\pi` |
| Ρ | `P` | ρ | `\rho` |
| Σ | `\Sigma` | σ | `\sigma` |
| Τ | `T` | τ | `\tau` |
| Υ | `\Upsilon` | υ | `\upsilon` |
| Φ | `\Phi` | φ | `\phi` |
| Χ | `X` | χ | `\chi` |
| Ψ | `\Psi` | ψ | `\psi` |
| Ω | `\Omega` | ω | `\omega` |
# References
You can find more information about Markdown, CommonMark, GitHub Flavored Markdown, LaTeX Math, and
Mermaid at the following links:
- [CommonMark](https://commonmark.org/)
- [GitHub Flavored Markdown](https://github.github.com/gfm/)
- [LaTeX](https://www.latex-project.org/)
- [Mermaid](https://mermaid.js.org/)
+41
View File
@@ -0,0 +1,41 @@
## Privacy Policy of Open Note
Flux is an open-source Android app developed by Ronit Chinda.
The source code is available on GitHub under the GPL-3.0 license.
### Data Collection
Flux does not collect any personal or confidential information such as addresses, names, or email addresses.
### Permissions Requested
The app requires the following permissions, as listed in the `AndroidManifest.xml` file:
https://github.com/chindaronit/Flux/blob/master/app/src/main/AndroidManifest.xml
| Permission | Purpose |
|---------------------------------|---------------------------------------------------------------------------------------------------------------|
| `android.permission.USE_BIOMETRIC` | Enables biometric authentication to provide secure access to protected notes |
| `android.permission.INTERNET` | Allows network connectivity for loading web content (images, videos, and other media) when rendering markdown |
| `android.permission.RECEIVE_BOOT_COMPLETED` | Allows the app to receive the system broadcast after device boot, enabling it to restart scheduled tasks such as reminders, alarms, or background sync services |
| `android.permission.POST_NOTIFICATIONS` | Grants permission to display notifications to the user (required on Android 13+), used for reminders, updates, or alerting about scheduled events |
| `android.permission.SCHEDULE_EXACT_ALARM` | Allows the app to schedule precise alarms that trigger at exact times (e.g., note reminders, task deadlines), bypassing battery optimizations that delay inexact alarms |
### Dependencies
The app uses the following dependencies:
- **Room**: For local database management.
- **Hilt**: For dependency injection.
- **Compose**: For building the UI.
- **CommonMark**: For markdown rendering and parsing.
### Data Sharing
Flux does not share any personal or sensitive user data with third parties.
### Data Deletion
Usually all data is stored locally and can be cleared by the user at any time.
---
If you have any questions about this policy or personal information protection, please send your inquiries, opinions, or suggestions to: chindaronit04@gmail.com
+1 -1
View File
@@ -75,7 +75,7 @@ To build and run this application, you need to install the latest version of And
you can clone this repository from GitHub and open it in Android Studio.
```bash
git clone https://github.com/YangDai2003/OpenNote.git
git clone git@github.com:chindaronit/Flux.git
```
In Android Studio, select `Run > Run 'app'` to start the application.
+4 -2
View File
@@ -15,8 +15,8 @@ android {
applicationId = "com.flux"
minSdk = 29
targetSdk = 36
versionCode = 5
versionName = "3.0"
versionCode = 7
versionName = "3.1.1"
}
dependenciesInfo {
@@ -92,6 +92,8 @@ dependencies {
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.compose.foundation.layout)
// Hilt
ksp(libs.hilt.android.compiler)
+1
View File
@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<application
android:name=".di.Flux"
+37 -12
View File
@@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -22,6 +24,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flux.navigation.AppNavHost
import com.flux.navigation.Loader
import com.flux.other.Constants.Other
import com.flux.other.createNotificationChannel
import com.flux.ui.effects.ScreenEffect
import com.flux.ui.theme.FluxTheme
@@ -34,6 +37,9 @@ import com.flux.ui.viewModel.SettingsViewModel
import com.flux.ui.viewModel.TodoViewModel
import com.flux.ui.viewModel.WorkspaceViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -73,18 +79,11 @@ class MainActivity : AppCompatActivity() {
// Stop splash screen when settings are loaded
LaunchedEffect(settings.isLoading) { keepSplashScreen.value = settings.isLoading }
// Snackbar effect
LaunchedEffect(Unit) {
workspaceViewModel.effect.collect { effect ->
if (effect is ScreenEffect.ShowSnackBarMessage) {
snackBarHostState.currentSnackbarData?.dismiss()
snackBarHostState.showSnackbar(
message = effect.message,
duration = SnackbarDuration.Short
)
}
}
}
HandleSideEffects(
snackBarHostState,
workspaceViewModel.effect,
settingsViewModel.effect
)
if (!settings.isLoading) {
FluxTheme(settings) {
@@ -117,4 +116,30 @@ class MainActivity : AppCompatActivity() {
}
}
}
}
@Composable
fun HandleSideEffects(
snackbarHostState: SnackbarHostState,
vararg effectFlows: Flow<ScreenEffect>
) {
LaunchedEffect(Other.SIDE_EFFECT_KEY) {
effectFlows.forEach { effectFlow ->
effectFlow
.onEach { effect ->
if (effect is ScreenEffect.ShowSnackBarMessage) {
val result = snackbarHostState.showSnackbar(
message = effect.message,
actionLabel = "Dismiss",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}
.launchIn(this)
}
}
}
@@ -34,7 +34,7 @@ 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],
version = 4,
version = 5,
exportSchema = false
)
@TypeConverters(Converter::class)
@@ -322,3 +322,9 @@ val MIGRATION_3_4 = object : Migration(3, 4) {
}
}
}
val Migration_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE SettingsModel ADD COLUMN backupFrequency INTEGER NOT NULL DEFAULT 0")
}
}
@@ -26,5 +26,6 @@ data class SettingsModel(
val isLintValid: Boolean = false,
val isLineNumbersVisible: Boolean = false,
val startWithReadView: Boolean = false,
val storageRootUri: String? = null
val storageRootUri: String? = null,
val backupFrequency: Int = 0
)
+2 -1
View File
@@ -16,6 +16,7 @@ import com.flux.data.database.FluxDatabase
import com.flux.data.database.MIGRATION_1_2
import com.flux.data.database.MIGRATION_2_3
import com.flux.data.database.MIGRATION_3_4
import com.flux.data.database.Migration_4_5
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -35,7 +36,7 @@ object DataModule {
FluxDatabase::class.java,
"FluxDatabase"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, Migration_4_5)
.build()
@Singleton
@@ -19,9 +19,9 @@ import com.flux.ui.screens.journal.EditJournal
import com.flux.ui.screens.notes.EditLabels
import com.flux.ui.screens.notes.NoteDetails
import com.flux.ui.screens.settings.About
import com.flux.ui.screens.settings.Backup
import com.flux.ui.screens.settings.Contact
import com.flux.ui.screens.settings.Customize
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.Privacy
@@ -171,8 +171,8 @@ val SettingsScreens =
NavRoutes.Contact.route to { navController, _, states, _ ->
Contact(navController, states.settings.data.cornerRadius)
},
NavRoutes.Backup.route to { navController, _, states, viewModels ->
Backup(navController, states.settings.data.cornerRadius, viewModels.backupViewModel)
NavRoutes.Backup.route to { navController, snackbarHostState, states, viewModels ->
Data(navController, states.settings.data.cornerRadius, states.settings, snackbarHostState, viewModels.backupViewModel, viewModels.settingsViewModel::onEvent)
},
NavRoutes.Editor.route to { navController, _, states, viewModels ->
Editor(navController, states.settings, viewModels.settingsViewModel::onEvent)
@@ -0,0 +1,123 @@
package com.flux.other
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.os.Build
import androidx.core.content.ContextCompat
import java.io.File
class AudioRecorder(private val context: Context) {
private var recorder: MediaRecorder? = null
private var tempFile: File? = null
private var isRecording = false
fun hasMicPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
// Start recording into a temp cache file
fun startRecording() {
if (isRecording) return
tempFile = File(
context.cacheDir,
"temp_audio_${System.currentTimeMillis()}.m4a"
)
val mediaRecorder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
MediaRecorder()
}
recorder = mediaRecorder.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setAudioEncodingBitRate(128_000)
setAudioSamplingRate(44_100)
setOutputFile(tempFile!!.absolutePath)
prepare()
start()
}
isRecording = true
}
// Stop recording and return temp file
fun stopRecording(): File? {
if (!isRecording) return null
return try {
recorder?.apply {
stop()
release()
}
val file = tempFile
recorder = null
tempFile = null
isRecording = false
file
} catch (_: Exception) {
// Cleanup corrupted file
tempFile?.delete()
recorder = null
tempFile = null
isRecording = false
null
}
}
// Delete temp recording without saving
fun deleteRecording() {
try {
recorder?.apply {
stop()
release()
}
} catch (_: Exception) {
// Ignore stop failure if not started
}
tempFile?.delete()
recorder = null
tempFile = null
isRecording = false
}
// Release resources (lifecycle safety)
fun release() {
try {
recorder?.release()
} catch (_: Exception) {}
recorder = null
tempFile = null
isRecording = false
}
// For waveform UI
fun getAmplitude(): Int {
return recorder?.maxAmplitude ?: 0
}
}
@@ -0,0 +1,40 @@
package com.flux.other
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
class BackupManager(
private val workManager: WorkManager
) {
fun scheduleBackup(frequency: BackupFrequency) {
if (frequency == BackupFrequency.NEVER) {
workManager.cancelUniqueWork(BACKUP_WORK_NAME)
return
}
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.build()
val backupRequest = PeriodicWorkRequestBuilder<BackupWorker>(
frequency.days.toLong(), TimeUnit.DAYS
)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
BACKUP_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
backupRequest
)
}
companion object {
const val BACKUP_WORK_NAME = "database_backup_work"
const val BACKUP_FREQUENCY_KEY = "backup_frequency"
}
}
@@ -0,0 +1,82 @@
package com.flux.other
import android.content.Context
import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.flux.R
import com.flux.data.database.FluxBackup
import com.flux.data.model.SettingsModel
import com.flux.di.DataModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.OutputStreamWriter
enum class BackupFrequency(val days: Int, val textRes: Int) {
NEVER(0, R.string.never),
DAILY(1, R.string.daily),
WEEKLY(7, R.string.weekly),
MONTHLY(30, R.string.monthly);
}
class BackupWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
val context = applicationContext
val fluxDatabase = DataModule.provideFluxDatabase(context)
val settingsDao = DataModule.provideSettingsDao(fluxDatabase)
val notesDao = DataModule.provideNotesDao(fluxDatabase)
val workspaceDao = DataModule.provideWorkspaceDao(fluxDatabase)
val todoDao = DataModule.provideTodoDao(fluxDatabase)
val habitDao = DataModule.provideHabitDao(fluxDatabase)
val habitInstanceDao = DataModule.provideHabitInstanceDao(fluxDatabase)
val journalDao = DataModule.provideJournalDao(fluxDatabase)
val labelDao = DataModule.provideLabelDao(fluxDatabase)
val eventDao = DataModule.provideEventDao(fluxDatabase)
val eventInstanceDao = DataModule.provideEventInstanceDao(fluxDatabase)
val settings = settingsDao.loadSetting()
val rootUri = settings?.storageRootUri?.toUri()?: "".toUri()
val openNoteDir = getOrCreateDirectory(context, rootUri, Constants.File.FLUX)
val backupDir = openNoteDir?.let { dir ->
getOrCreateDirectory(context, dir.uri, Constants.File.FLUX_BACKUP)
}
backupDir?.let { dir ->
val backup = FluxBackup(
workspaces = workspaceDao.getAll(),
notes = notesDao.loadAllNotes(),
todos = todoDao.loadAllLists(),
habits = habitDao.loadAllHabits(),
habitInstances = habitInstanceDao.loadAllInstances(),
journals = journalDao.loadAllEntries(),
labels = labelDao.getAll(),
events = eventDao.loadAllEvents(),
eventInstances = eventInstanceDao.getAll(),
settings = settingsDao.loadSetting()?: SettingsModel()
)
val json = Json.encodeToString(FluxBackup.serializer(), backup)
val fileName = "${System.currentTimeMillis()}.json"
val file = dir.createFile("application/json", fileName)
file?.let { docFile ->
context.contentResolver.openOutputStream(docFile.uri)
?.use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(json)
}
}
}
}
Result.success()
} catch (_: Exception) {
Result.failure()
}
}
}
@@ -15,7 +15,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
@@ -49,7 +48,6 @@ class BootReceiver : BroadcastReceiver() {
val events = eventRepository.loadAllEvents()
return habits.filter { it.isLive() } + events
}
}
@@ -19,6 +19,11 @@ object Constants {
const val FLUX_IMAGES = "Images"
const val FLUX_AUDIO = "Audio"
const val FLUX_VIDEOS = "Videos"
const val FLUX_BACKUP = "Backup"
}
object Other {
const val SIDE_EFFECT_KEY = "side_effect"
}
object Editor {
@@ -58,10 +58,8 @@ import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.parser.IncludeSourceSpans
import org.commonmark.parser.Parser
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter
import androidx.core.net.toUri
fun Int.toHexColor(): String {
return String.format("#%06X", 0xFFFFFF and this)
}
@@ -82,6 +82,7 @@ import com.flux.data.model.getSpacesList
import com.flux.other.HeaderNode
import com.flux.other.icons
import com.flux.other.workspaceIconList
import com.flux.ui.screens.events.formatCustom
import com.flux.ui.screens.events.formatMonthly
import com.flux.ui.screens.events.formatOnce
import com.flux.ui.screens.events.formatYearly
@@ -479,7 +480,7 @@ fun RecurrenceBottomSheet(
value = rule.everyXDays.toString(),
onValueChange = { new -> new.toIntOrNull()?.let { tempRule = rule.copy(everyXDays = it) } },
modifier = Modifier.padding(start = 8.dp),
label = { Text(stringResource(R.string.recurrence_every_x_days)) },
label = { Text(formatCustom(rule)) },
singleLine = true
)
}
@@ -0,0 +1,102 @@
package com.flux.ui.components
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.cos
import kotlin.math.sin
/**
* A custom shape with curly edges, resembling a circle with sinusoidal indentations.
*
* This shape creates an outline that appears as a circle with "curly" or "wavy"
* edges, achieved by applying a sine wave function to the circle's radius. The
* amplitude and the number of curls (waves) can be adjusted.
*
* @property curlAmplitude The amplitude of the sine wave, controlling the depth of the curls.
* A higher value results in deeper curls. Defaults to 16.0.
* @property curlCount The number of curls (waves) around the circle's circumference.
* A higher count results in more frequent curls. Defaults to 12.
*/
class CurlyCornerShape(
private val curlAmplitude: Double = 16.0,
private val curlCount: Int = 12,
) : CornerBasedShape(
topStart = ZeroCornerSize,
topEnd = ZeroCornerSize,
bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize
) {
/**
* Calculates the x and y coordinates of a point on the curly circle at a given angle.
*
* @param centerX The x-coordinate of the center of the circle.
* @param centerY The y-coordinate of the center of the circle.
* @param baseRadius The base radius of the circle.
* @param amplitude The amplitude of the sine wave.
* @param angle The angle in radians.
* @return A Pair containing the x and y coordinates of the point.
*/
private fun calculateCurlyCirclePoint(
centerX: Double,
centerY: Double,
baseRadius: Double,
amplitude: Double,
angle: Double,
): Pair<Double, Double> {
// Calculate the radius with the sine wave applied.
val radius = baseRadius + amplitude * sin(curlCount * angle)
// Calculate x and y coordinates using polar coordinates.
val x = centerX + radius * cos(angle)
val y = centerY + radius * sin(angle)
return Pair(x, y)
}
override fun createOutline(
size: Size,
topStart: Float,
topEnd: Float,
bottomEnd: Float,
bottomStart: Float,
layoutDirection: LayoutDirection
): Outline {
val centerX = size.width / 2.0
val centerY = size.height / 2.0
val baseRadius = centerX - curlAmplitude
val path = Path()
// Start at the rightmost point
val startPoint = calculateCurlyCirclePoint(centerX, centerY, baseRadius, curlAmplitude, 0.0)
path.moveTo(startPoint.first.toFloat(), startPoint.second.toFloat())
// Iterate through 360 degrees to draw the curly circle
for (angleDegrees in 1..360) {
// Convert the angle to radians.
val angleRadians = Math.toRadians(angleDegrees.toDouble())
// calculate the current point
val currentPoint =
calculateCurlyCirclePoint(centerX, centerY, baseRadius, curlAmplitude, angleRadians)
path.lineTo(currentPoint.first.toFloat(), currentPoint.second.toFloat())
}
path.close()
return Outline.Generic(path)
}
override fun copy(
topStart: CornerSize,
topEnd: CornerSize,
bottomEnd: CornerSize,
bottomStart: CornerSize,
): CurlyCornerShape = CurlyCornerShape(
curlAmplitude = this.curlAmplitude,
curlCount = this.curlCount
)
}
@@ -1,13 +1,23 @@
package com.flux.ui.components
import android.Manifest
import android.content.Context
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -18,6 +28,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -28,6 +39,7 @@ import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.automirrored.outlined.LabelImportant
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AudioFile
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.Edit
@@ -35,10 +47,14 @@ import androidx.compose.material.icons.filled.EditCalendar
import androidx.compose.material.icons.filled.FilePresent
import androidx.compose.material.icons.filled.FontDownload
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.NewLabel
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.StopCircle
import androidx.compose.material.icons.filled.SubdirectoryArrowRight
import androidx.compose.material.icons.filled.TextFields
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.outlined.FormatListNumbered
import androidx.compose.material.icons.outlined.NotificationsActive
import androidx.compose.material3.AlertDialog
@@ -70,15 +86,22 @@ import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
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.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -86,13 +109,20 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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.compose.ui.window.DialogProperties
import androidx.core.net.toUri
import com.flux.R
import com.flux.data.model.LabelModel
import com.flux.other.AudioRecorder
import com.flux.other.ExportType
import com.flux.ui.theme.FONTS
import com.flux.ui.theme.completed
import com.flux.ui.theme.failed
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
@@ -1124,4 +1154,396 @@ fun TaskDialog(
}
}
)
}
@Composable
fun RecordAudioDialog(
context: Context,
recorder: AudioRecorder,
onSave: (Uri?) -> Unit,
onDismissRequest: () -> Unit,
) {
var isRecording by remember { mutableStateOf(false) }
var recordedFile by remember { mutableStateOf<File?>(null) }
val hasRecorded = recordedFile != null
var recordedTime by remember { mutableLongStateOf(0L) }
val permissionRequired = stringResource(R.string.permission_required)
// Permission launcher
val permissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
recorder.startRecording()
isRecording = true
} else {
Toast.makeText(context, permissionRequired, Toast.LENGTH_SHORT).show()
}
}
Dialog(
onDismissRequest = {
if (isRecording) { recorder.deleteRecording() }
onDismissRequest()
}
) {
Card(modifier = Modifier.size(320.dp)) {
Column(
modifier = Modifier.padding(16.dp).fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (!isRecording&& !hasRecorded) {
Spacer(Modifier.height(8.dp))
Text(stringResource(R.string.record_audio), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold))
Spacer(Modifier.height(32.dp))
// RECORD/ STOP
IconButton({
if (recorder.hasMicPermission(context)) {
recorder.startRecording()
isRecording = true
} else {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}, colors = IconButtonDefaults.iconButtonColors(
containerColor = failed,
contentColor = Color.White
), modifier = Modifier.size(120.dp),
shape = CurlyCornerShape(14.0, 6)
) {
Icon(Icons.Default.Mic, null, modifier = Modifier.size(75.dp))
}
Spacer(Modifier.height(32.dp))
Text(stringResource(R.string.tap_to_start), style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Light))
}
if(isRecording && !hasRecorded) {
StudioRecorderUI(recorder) {
recordedTime=it
recordedFile = recorder.stopRecording()
isRecording = false
}
}
if(hasRecorded){
Spacer(Modifier.height(8.dp))
Text(formatDuration(recordedTime), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold))
Spacer(Modifier.height(16.dp))
// RECORD/ STOP
IconButton({}, colors = IconButtonDefaults.iconButtonColors(
containerColor = failed,
contentColor = Color.White
), modifier = Modifier.size(120.dp),
shape = CurlyCornerShape(curlCount = 8, curlAmplitude = 12.0)) {
Icon(Icons.Default.AudioFile, null, modifier = Modifier.size(80.dp))
}
Spacer(Modifier.height(12.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Retry
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
recorder.deleteRecording()
recordedFile=null
recorder.startRecording()
isRecording = true
},
colors = ButtonDefaults.buttonColors(
containerColor = failed,
contentColor = Color.White
)
) {
Icon(Icons.Default.Refresh, null)
Text(stringResource(R.string.retry), modifier = Modifier.padding(start = 8.dp))
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onSave(recordedFile?.toUri())
onDismissRequest()
},
colors = ButtonDefaults.buttonColors(
containerColor = completed,
contentColor = Color.White
)
) {
Icon(Icons.Default.Verified, null)
Text(stringResource(R.string.save), modifier = Modifier.padding(start = 8.dp))
}
}
}
}
}
}
}
@Composable
fun StudioRecorderUI(
recorder: AudioRecorder,
onStop: (Long) -> Unit
) {
val amplitudes = remember { MutableList(120) { 0.02f }.toMutableStateList() }
var elapsedMs by remember { mutableLongStateOf(0L) }
LaunchedEffect(Unit) {
val start = System.currentTimeMillis()
var smoothedAmp = 0f
while (true) {
val raw = recorder.getAmplitude() / 32767f
// Low-pass smoothing filter
smoothedAmp = (raw * 0.2f) + (smoothedAmp * 0.8f)
// Silence floor so waveform never vanishes
val visualAmp = maxOf(smoothedAmp, 0.02f)
amplitudes.add(visualAmp)
if (amplitudes.size > 200) {
amplitudes.removeAt(0)
}
elapsedMs = System.currentTimeMillis() - start
delay(50)
}
}
Column(
modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// TIMELINE + WAVE
Box(modifier = Modifier.fillMaxWidth().height(160.dp)) {
TimelineWaveform(amplitudes, elapsedMs)
// CENTER RED CURSOR
Box(
modifier = Modifier
.width(2.dp)
.fillMaxHeight()
.align(Alignment.Center)
.background(Color.Red)
)
}
Spacer(Modifier.height(6.dp))
// TIMER
Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(12.dp).background(Color.Red, CircleShape))
Spacer(Modifier.width(8.dp))
Text(text = formatDuration(elapsedMs), fontSize = 22.sp)
}
Spacer(Modifier.height(8.dp))
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = {onStop(elapsedMs)},
modifier = Modifier.size(80.dp)
) {
Icon(
Icons.Default.StopCircle,
contentDescription = null,
tint = failed,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
fun TimelineWaveform(
amplitudes: List<Float>,
elapsedMs: Long
) {
val onSurface = MaterialTheme.colorScheme.onSurface
Canvas(modifier = Modifier.fillMaxWidth().height(160.dp)) {
val width = size.width
val height = size.height
val centerX = width / 2f
val centerY = height / 2f
val pxPerSecond = 80f
val samplesPerSecond = 20f
val pxPerSample = pxPerSecond / samplesPerSecond
val secondsElapsed = elapsedMs / 1000f
// ==============================
// WAVEFORM (PAST → LEFT)
// ==============================
amplitudes.forEachIndexed { index, rawAmp ->
val x = centerX - (amplitudes.size - index) * (pxPerSample * 1.6f)
if (x >= 0f) {
// Boost amplitude visually
val amp = rawAmp.coerceIn(0f, 1f)
val visualHeight =
(amp * height * 1.2f).coerceAtMost(height * 0.95f)
val halfBar = visualHeight / 2f
// Dynamic alpha for clarity
val alpha =
0.3f + (amp * 0.7f)
drawLine(
color = onSurface.copy(alpha = alpha),
start = Offset(x, centerY - halfBar),
end = Offset(x, centerY + halfBar),
strokeWidth = 3f,
cap = StrokeCap.Round
)
}
}
// ==============================
// FUTURE PLACEHOLDER (RIGHT DOTS)
// ==============================
var futureX = centerX + pxPerSample
while (futureX < width) {
drawLine(
color = Color.Gray.copy(alpha = 0.4f),
start = Offset(futureX, centerY - 4f),
end = Offset(futureX, centerY + 4f),
strokeWidth = 3f,
cap = StrokeCap.Round
)
futureX += pxPerSample * 2
}
// ==============================
// TIMELINE TICKS + LABELS
// ==============================
val offsetPx = (secondsElapsed * pxPerSecond) % pxPerSecond
val paint = android.graphics.Paint().apply {
color = android.graphics.Color.BLACK
textSize = 28f
textAlign = android.graphics.Paint.Align.CENTER
}
// Prevent duplicate labels
val drawnSeconds = mutableSetOf<Int>()
// -------- PAST (LEFT) --------
var pastTickIndex = 0
var tickX = centerX - offsetPx
while (tickX > 0f) {
val timeAtTick = secondsElapsed - pastTickIndex
val secondInt = timeAtTick.toInt()
// Tick line
drawLine(
color = Color.Gray,
start = Offset(tickX, 0f),
end = Offset(tickX, 24f),
strokeWidth = 2f
)
// Draw label only if:
// 1. Time exists (>= 0)
// 2. Even second
// 3. Not already drawn
if (secondInt >= 0 &&
secondInt % 2 == 0 &&
drawnSeconds.add(secondInt)
) {
drawContext.canvas.nativeCanvas.drawText(
formatTimelineLabel(secondInt),
tickX,
48f,
paint
)
}
tickX -= pxPerSecond
pastTickIndex++
}
// -------- FUTURE (RIGHT) --------
var futureTickIndex = 1
var futureTickX =
centerX + (pxPerSecond - offsetPx)
while (futureTickX < width) {
val futureSecond =
(secondsElapsed + futureTickIndex).toInt()
drawLine(
color = onSurface,
start = Offset(futureTickX, 0f),
end = Offset(futureTickX, 24f),
strokeWidth = 2f
)
if (futureSecond % 2 == 0 &&
drawnSeconds.add(futureSecond)
) {
drawContext.canvas.nativeCanvas.drawText(
formatTimelineLabel(futureSecond),
futureTickX,
48f,
paint
)
}
futureTickX += pxPerSecond
futureTickIndex++
}
// ==============================
// CURSOR
// ==============================
drawLine(
color = failed,
start = Offset(centerX, 0f),
end = Offset(centerX, height),
strokeWidth = 3f
)
}
}
fun formatTimelineLabel(seconds: Int): String {
val min = seconds / 60
val sec = seconds % 60
return "%02d:%02d".format(min, sec)
}
fun formatDuration(ms: Long): String {
val totalSec = ms / 1000
val min = totalSec / 60
val sec = totalSec % 60
return "%02d:%02d".format(min, sec)
}
@@ -20,6 +20,7 @@ import androidx.compose.material.icons.automirrored.outlined.FormatIndentIncreas
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.automirrored.outlined.List
import androidx.compose.material.icons.outlined.AddChart
import androidx.compose.material.icons.outlined.AudioFile
import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.DataArray
@@ -99,6 +100,7 @@ fun MarkdownEditorRow(
onLinkButtonClick: () -> Unit,
onImageButtonClick: () -> Unit,
onAudioButtonClick: () -> Unit,
onRecordAudioClick: () -> Unit,
onVideoButtonClick: () -> Unit
) {
@@ -366,6 +368,12 @@ fun MarkdownEditorRow(
CustomIconButton(
imageVector = Icons.Outlined.Mic,
contentDescription = "Audio",
onClick = onRecordAudioClick
)
CustomIconButton(
imageVector = Icons.Outlined.AudioFile,
contentDescription = "Audio",
onClick = onAudioButtonClick
)
@@ -3,5 +3,6 @@ package com.flux.ui.events
import com.flux.data.model.SettingsModel
sealed class SettingEvents {
data class UpdateSettings(val data: SettingsModel) : SettingEvents()
data class UpdateSettings(val data: SettingsModel): SettingEvents()
data object ResetDatabase: SettingEvents()
}
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastJoinToString
import androidx.navigation.NavController
import com.flux.data.model.JournalModel
import com.flux.other.AudioRecorder
import com.flux.other.Constants
import com.flux.other.HeaderNode
import com.flux.other.ensureStorageRoot
@@ -55,6 +56,7 @@ import com.flux.ui.components.ListDialog
import com.flux.ui.components.MarkdownEditorRow
import com.flux.ui.components.NotesInfoBottomSheet
import com.flux.ui.components.OutlineBottomSheet
import com.flux.ui.components.RecordAudioDialog
import com.flux.ui.components.ShareDialog
import com.flux.ui.components.TableDialog
import com.flux.ui.components.TaskDialog
@@ -116,7 +118,9 @@ fun EditJournal(
var showTableDialog by rememberSaveable { mutableStateOf(false) }
var showListDialog by rememberSaveable { mutableStateOf(false) }
val isReadView by remember { derivedStateOf { pagerState.currentPage == 1 } }
var showAudioRecorder by rememberSaveable { mutableStateOf(false) }
var readWebView by remember { mutableStateOf<WebView?>(null) }
val recorder = AudioRecorder(context)
val rootPicker =
rememberLauncherForActivityResult(
@@ -223,6 +227,15 @@ fun EditJournal(
onListButtonClick = { showListDialog = true },
onTaskButtonClick = { showTaskDialog = true },
onLinkButtonClick = { showLinkDialog = true },
onRecordAudioClick = {
ensureStorageRoot(
scope = scope,
settingsViewModel = settingsViewModel,
rootPicker = rootPicker
) {
showAudioRecorder=true
}
},
onImageButtonClick = {
ensureStorageRoot(
scope = scope,
@@ -368,6 +381,12 @@ fun EditJournal(
})
}
if(showAudioRecorder){
RecordAudioDialog(context, recorder, {onJournalEvents(JournalEvents.ImportAudio(context, it!!, contentState))}) {
showAudioRecorder=false
}
}
NotesInfoBottomSheet(
isVisible = showAboutNotes,
words = aboutJournal.wordCountWithPunctuation,
@@ -96,6 +96,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import com.flux.other.AudioRecorder
import com.flux.ui.components.RecordAudioDialog
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
@@ -121,6 +123,7 @@ fun NoteDetails(
val hasContent = remember(note.notesId) {
note.title.isNotBlank() || note.description.isNotBlank()
}
val recorder = AudioRecorder(context)
val pagerState = rememberPagerState(
initialPage = if (startWithReadView && hasContent) 1 else 0,
@@ -141,6 +144,7 @@ fun NoteDetails(
var showTaskDialog by rememberSaveable { mutableStateOf(false) }
var showTableDialog by rememberSaveable { mutableStateOf(false) }
var showListDialog by rememberSaveable { mutableStateOf(false) }
var showAudioRecorder by rememberSaveable { mutableStateOf(false) }
var showSelectLabels by rememberSaveable { mutableStateOf(false) }
var isPinned by rememberSaveable(note.notesId) { mutableStateOf(note.isPinned) }
val noteLabelIds = rememberSaveable {
@@ -275,6 +279,15 @@ fun NoteDetails(
onListButtonClick = { showListDialog = true },
onTaskButtonClick = { showTaskDialog = true },
onLinkButtonClick = { showLinkDialog = true },
onRecordAudioClick = {
ensureStorageRoot(
scope = scope,
settingsViewModel = settingsViewModel,
rootPicker = rootPicker
) {
showAudioRecorder=true
}
},
onImageButtonClick = {
ensureStorageRoot(
scope = scope,
@@ -484,6 +497,12 @@ fun NoteDetails(
}
}
if(showAudioRecorder){
RecordAudioDialog(context, recorder, {onNotesEvents(NotesEvents.ImportAudio(context, it!!, contentState))}) {
showAudioRecorder=false
}
}
NotesInfoBottomSheet(
isVisible = showAboutNotes,
words = aboutNotes.wordCountWithPunctuation,
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.LazyListScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
@@ -65,13 +66,12 @@ fun LazyListScope.notesHomeItems(
item {
val columns = if (isGridView) 2 else 1
val rowCount = ceil(pinnedNotes.size / columns.toFloat()).toInt()
val gridHeight =
rowCount * 300.dp + if (pinnedNotes.isNotEmpty()) 200.dp else 0.dp
val gridHeight = rowCount * 300.dp + if (pinnedNotes.isNotEmpty() && unPinnedNotes.isEmpty()) 200.dp else 25.dp
Box(
Modifier
.fillMaxWidth()
.height(gridHeight)
.heightIn(max=gridHeight)
) {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(columns),
@@ -8,9 +8,12 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Article
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.Code
import androidx.compose.material.icons.rounded.DeveloperMode
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.IosShare
import androidx.compose.material.icons.rounded.PrivacyTip
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -110,6 +113,63 @@ fun About(navController: NavController, radius: Int) {
}
)
}
item {
Spacer(Modifier.height(24.dp))
SettingOption(
title = stringResource(R.string.user_guide_title),
description = stringResource(R.string.user_guide_description),
icon = Icons.Outlined.Info,
radius = shapeManager(radius = radius, isFirst = true),
actionType = ActionType.LINK,
linkClicked = {
val intent = Intent(
Intent.ACTION_VIEW,
"https://github.com/chindaronit/Flux/blob/master/Guide.md".toUri()
)
context.startActivity(intent)
}
)
}
item {
SettingOption(
title = stringResource(R.string.privacy_policy_title),
description = stringResource(R.string.privacy_policy_description),
icon = Icons.Rounded.PrivacyTip,
radius = shapeManager(radius = radius, isLast = true),
actionType = ActionType.LINK,
linkClicked = {
val intent = Intent(
Intent.ACTION_VIEW,
"https://github.com/chindaronit/Flux/blob/master/PRIVACY_POLICY.md".toUri()
)
context.startActivity(intent)
}
)
}
item {
Spacer(Modifier.height(24.dp))
SettingOption(
title = stringResource(R.string.share),
description = stringResource(R.string.share_description),
icon = Icons.Rounded.IosShare,
radius = shapeManager(radius = radius, isBoth = true),
actionType = ActionType.LINK,
linkClicked = {
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.setType("text/plain")
sendIntent.putExtra(
Intent.EXTRA_TITLE, context.getString(R.string.app_name)
)
sendIntent.putExtra(
Intent.EXTRA_TEXT, context.getString(R.string.shareContent)
)
context.startActivity(Intent.createChooser(sendIntent, null))
}
)
}
}
}
}
@@ -1,123 +0,0 @@
package com.flux.ui.screens.settings
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backup
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
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.other.canScheduleReminder
import com.flux.other.isNotificationPermissionGranted
import com.flux.other.openAppNotificationSettings
import com.flux.other.requestExactAlarmPermission
import com.flux.ui.components.ActionType
import com.flux.ui.components.BasicScaffold
import com.flux.ui.components.SettingOption
import com.flux.ui.components.shapeManager
import com.flux.ui.viewModel.BackupViewModel
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Backup(
navController: NavController,
radius: Int,
backupViewModel: BackupViewModel
) {
val context = LocalContext.current
// EXPORT launcher
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { backupViewModel.exportBackup(context, it) }
}
// IMPORT launcher
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { backupViewModel.importBackup(context, it) }
}
// Observe result - Collect SharedFlow properly
LaunchedEffect(Unit) {
backupViewModel.backupResult.collect { result ->
if (result.isSuccess) {
Toast.makeText(context, "Operation successful!", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "Operation failed.", Toast.LENGTH_LONG).show()
}
}
}
// ... rest of your UI
BasicScaffold(
title = stringResource(R.string.Backup),
onBackClicked = {
navController.navigateUp()
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
item {
SettingOption(
title = stringResource(R.string.Backup),
description = stringResource(R.string.Backup_Description),
icon = Icons.Rounded.Backup,
radius = shapeManager(radius = radius, isFirst = true),
actionType = ActionType.CUSTOM,
onCustomClick = { exportLauncher.launch("flux-backup.json") }
)
}
item {
SettingOption(
title = stringResource(R.string.Restore),
description = stringResource(R.string.Restore_Description),
icon = Icons.Rounded.Restore,
radius = shapeManager(radius = radius, isLast = true),
actionType = ActionType.CUSTOM,
onCustomClick = {
if (!canScheduleReminder(context)) {
Toast.makeText(
context,
context.getText(R.string.Reminder_Permission),
Toast.LENGTH_SHORT
).show()
requestExactAlarmPermission(context)
}
if (!isNotificationPermissionGranted(context)) {
Toast.makeText(
context,
context.getText(R.string.Notification_Permission),
Toast.LENGTH_SHORT
).show()
openAppNotificationSettings(context)
}
if (canScheduleReminder(context) && isNotificationPermissionGranted(context)) {
importLauncher.launch(arrayOf("application/json"))
}
}
)
}
}
}
}
@@ -19,10 +19,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Colorize
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Spellcheck
import androidx.compose.material.icons.rounded.FontDownload
import androidx.compose.material.icons.rounded.RoundedCorner
import androidx.compose.material.icons.rounded.ViewCompact
@@ -316,46 +314,6 @@ fun Customize(
)
}
item {
SettingOption(
title = stringResource(R.string.show_line_numbers),
description = stringResource(R.string.show_line_numbers_desc),
icon = Icons.Filled.FormatListNumbered,
radius = shapeManager(radius = settings.data.cornerRadius),
actionType = ActionType.SWITCH,
variable = settings.data.isLineNumbersVisible,
switchEnabled = {
onSettingsEvents(
SettingEvents.UpdateSettings(
settings.data.copy(
isLineNumbersVisible = it
)
)
)
}
)
}
item {
SettingOption(
title = stringResource(R.string.markdown_lint),
description = stringResource(R.string.markdown_lint_desc),
icon = Icons.Filled.Spellcheck,
radius = shapeManager(radius = settings.data.cornerRadius),
actionType = ActionType.SWITCH,
variable = settings.data.isLintValid,
switchEnabled = {
onSettingsEvents(
SettingEvents.UpdateSettings(
settings.data.copy(
isLintValid = it
)
)
)
}
)
}
item {
SettingOption(
title = stringResource(R.string.Hour_Format_24),
@@ -0,0 +1,268 @@
package com.flux.ui.screens.settings
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.CleaningServices
import androidx.compose.material.icons.outlined.EditCalendar
import androidx.compose.material.icons.rounded.Backup
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.work.WorkManager
import com.flux.R
import com.flux.other.BackupFrequency
import com.flux.other.BackupManager
import com.flux.other.canScheduleReminder
import com.flux.other.isNotificationPermissionGranted
import com.flux.other.openAppNotificationSettings
import com.flux.other.requestExactAlarmPermission
import com.flux.ui.components.ActionType
import com.flux.ui.components.DeleteAlert
import com.flux.ui.components.SettingOption
import com.flux.ui.components.shapeManager
import com.flux.ui.events.SettingEvents
import com.flux.ui.state.Settings
import com.flux.ui.viewModel.BackupViewModel
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Data(
navController: NavController,
radius: Int,
settings: Settings,
snackbarHostState: SnackbarHostState,
backupViewModel: BackupViewModel,
onSettingsEvents: (SettingEvents) -> Unit
) {
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
var showWarningDialog by remember { mutableStateOf(false) }
// EXPORT launcher
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { backupViewModel.exportBackup(context, it) }
}
// IMPORT launcher
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { backupViewModel.importBackup(context, it) }
}
// Observe result - Collect SharedFlow properly
LaunchedEffect(Unit) {
backupViewModel.backupResult.collect { result ->
if (result.isSuccess) {
Toast.makeText(context, "Operation successful!", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "Operation failed.", Toast.LENGTH_LONG).show()
}
}
}
// ... rest of your UI
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
title = { Text(stringResource(R.string.data_title)) },
navigationIcon = {
IconButton({navController.navigateUp()}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
){ innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
item {
SettingOption(
title = stringResource(R.string.Backup),
description = stringResource(R.string.Backup_Description),
icon = Icons.Rounded.Backup,
radius = shapeManager(radius = radius, isFirst = true),
actionType = ActionType.CUSTOM,
onCustomClick = { exportLauncher.launch("flux-backup.json") }
)
}
item {
SettingOption(
title = stringResource(R.string.Restore),
description = stringResource(R.string.Restore_Description),
icon = Icons.Rounded.Restore,
radius = shapeManager(radius = radius, isLast = true),
actionType = ActionType.CUSTOM,
onCustomClick = {
if (!canScheduleReminder(context)) {
Toast.makeText(
context,
context.getText(R.string.Reminder_Permission),
Toast.LENGTH_SHORT
).show()
requestExactAlarmPermission(context)
}
if (!isNotificationPermissionGranted(context)) {
Toast.makeText(
context,
context.getText(R.string.Notification_Permission),
Toast.LENGTH_SHORT
).show()
openAppNotificationSettings(context)
}
if (canScheduleReminder(context) && isNotificationPermissionGranted(context)) {
importLauncher.launch(arrayOf("application/json"))
}
}
)
}
item {
val workManager = remember { WorkManager.getInstance(context.applicationContext) }
val backupManager = remember(workManager) { BackupManager(workManager) }
fun mapDaysToSliderPosition(days: Int): Float = when (days) {
0 -> 0f
1 -> 1f
7 -> 2f
30 -> 3f
else -> 0f
}
fun mapSliderPositionToFrequency(position: Float): BackupFrequency = when (position) {
0f -> BackupFrequency.NEVER
1f -> BackupFrequency.DAILY
2f -> BackupFrequency.WEEKLY
3f -> BackupFrequency.MONTHLY
else -> BackupFrequency.NEVER
}
val sliderPosition = remember(settings.data.backupFrequency) {
mapDaysToSliderPosition(settings.data.backupFrequency)
}
val selectedFrequency = remember(sliderPosition) {
mapSliderPositionToFrequency(sliderPosition)
}
ListItem(
modifier = Modifier.padding(top = 8.dp),
leadingContent = {
Icon(
imageVector = Icons.Outlined.EditCalendar,
contentDescription = "Auto backup"
)
},
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
headlineContent = { Text(stringResource(R.string.backup_frequency)) },
supportingContent = { Text(text = stringResource(selectedFrequency.textRes)) }
)
Slider(
modifier = Modifier.padding(horizontal = 16.dp),
value = sliderPosition,
onValueChange = { newPosition ->
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
val newFrequency = mapSliderPositionToFrequency(newPosition)
onSettingsEvents(SettingEvents.UpdateSettings(settings.data.copy(backupFrequency = newFrequency.days)))
},
onValueChangeFinished = { backupManager.scheduleBackup(selectedFrequency) },
valueRange = 0f..3f,
steps = 2
)
}
item {
ListItem(
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
leadingContent = {
Icon(
imageVector = Icons.Outlined.CleaningServices,
contentDescription = "Reset"
)
},
headlineContent = { Text(stringResource(R.string.reset_database)) },
trailingContent = {
TextButton(
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick)
showWarningDialog = true
},
colors = ButtonDefaults.textButtonColors().copy(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Text(stringResource(R.string.reset))
}
},
supportingContent = {
Text(stringResource(R.string.clear_app_data))
}
)
}
}
if(showWarningDialog){
DeleteAlert(
onConfirmation = {
showWarningDialog=false
onSettingsEvents(SettingEvents.ResetDatabase) },
onDismissRequest = { showWarningDialog = false },
dialogTitle = stringResource(R.string.deleteDialogTitle),
dialogText = stringResource(R.string.deleteDialogText),
icon = Icons.Default.Delete
)
}
}
}
@@ -119,8 +119,8 @@ fun Settings(
item {
SettingCategory(
title = stringResource(R.string.Backup),
subTitle = stringResource(R.string.Backup_Description),
title = stringResource(R.string.data_title),
subTitle = stringResource(R.string.data_subtitle),
icon = Icons.Rounded.Backup,
shape = shapeManager(radius = settings.data.cornerRadius),
action = {
@@ -5,8 +5,8 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
val completed = Color(0xFF009B44)
val pending = Color(0xFFBE0000)
val failed = Color(0xFFFFD600)
val failed = Color(0xFFBE0000)
val pending = Color(0xFFFFD600)
private val primaryLight = Color(0xFF006A65)
private val onPrimaryLight = Color(0xFFFFFFFF)
@@ -3,28 +3,39 @@ package com.flux.ui.viewModel
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.flux.data.database.FluxDatabase
import com.flux.data.model.SettingsModel
import com.flux.data.repository.SettingsRepository
import com.flux.ui.effects.ScreenEffect
import com.flux.ui.events.SettingEvents
import com.flux.ui.state.Settings
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
val repository: SettingsRepository
val repository: SettingsRepository,
val database: FluxDatabase
) : ViewModel() {
private val _state: MutableStateFlow<Settings> = MutableStateFlow(Settings())
val state: StateFlow<Settings> = _state.asStateFlow()
private val _effect: Channel<ScreenEffect> = Channel()
val effect = _effect.receiveAsFlow()
init {
loadSettings()
init { loadSettings() }
private fun setEffect(builder: () -> ScreenEffect) {
val effectValue = builder()
viewModelScope.launch { _effect.send(effectValue) }
}
fun onEvent(event: SettingEvents) {
@@ -37,9 +48,8 @@ class SettingsViewModel @Inject constructor(
private fun reduce(event: SettingEvents) {
when (event) {
is SettingEvents.UpdateSettings -> {
updateSettings(event.data)
}
is SettingEvents.UpdateSettings -> { updateSettings(event.data) }
is SettingEvents.ResetDatabase -> { resetDatabase() }
}
}
@@ -47,10 +57,7 @@ class SettingsViewModel @Inject constructor(
updateState { it.copy(isLoading = true) }
viewModelScope.launch {
repository.loadSettings().collect { data ->
if (data != null){
println(data)
updateState { it.copy(isLoading = false, data = data) }
}
if (data != null){ updateState { it.copy(isLoading = false, data = data) } }
else updateState { it.copy(isLoading = false) }
}
}
@@ -69,4 +76,24 @@ class SettingsViewModel @Inject constructor(
repository.saveStorageRoot(uri)
}
}
fun resetDatabase() {
viewModelScope.launch {
runCatching {
withContext(Dispatchers.IO) {
database.clearAllTables()
}
}.onSuccess {
setEffect {
ScreenEffect.ShowSnackBarMessage("Reset completed")
}
}.onFailure { throwable ->
setEffect {
ScreenEffect.ShowSnackBarMessage(
"Failed to reset database: ${throwable.localizedMessage ?: "error"}"
)
}
}
}
}
}
+30 -10
View File
@@ -120,6 +120,14 @@
<string name="Backup_Description">Daten in einer Datei sichern</string>
<string name="Restore">Wiederherstellen</string>
<string name="Restore_Description">Daten aus einer Backup-Datei wiederherstellen</string>
<string name="reset">Zurücksetzen</string>
<string name="backup_frequency">Häufigkeit der automatischen Sicherung</string>
<string name="reset_database">Datenbank zurücksetzen</string>
<string name="clear_app_data">Alle App-Daten löschen</string>
<string name="never">Nie</string>
<string name="daily">Täglich</string>
<string name="weekly">Wöchentlich</string>
<string name="monthly">Monatlich</string>
<!-- Settings - About -->
<string name="About">Über</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">GitHub-Repository</string>
<string name="license">Lizenz</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Benutzerhandbuch</string>
<string name="user_guide_description">Lesen Sie das Handbuch, um die Anwendung vollständig zu nutzen.</string>
<string name="privacy_policy_title">Datenschutzrichtlinie</string>
<string name="privacy_policy_description">Datenschutzrichtlinie der Anwendung.</string>
<string name="share_title">Teilen</string>
<string name="share_description">Teilen Sie die Anwendung mit Freunden und Familie.</string>
<string name="data_title">Daten</string>
<string name="data_subtitle">Sicherung • Wiederherstellung</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Kontakt</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Fehler beim Import der Notiz</string>
<string name="find">Suchen</string>
<string name="replace">Ersetzen</string>
<string name="record_audio">Audio aufnehmen</string>
<string name="tap_to_start">Tippen zum Starten</string>
<string name="retry">Erneut versuchen</string>
<string name="permission_required">Bitte erteilen Sie die Aufnahmeberechtigung.</string>
<!-- To-Do -->
<string name="To_Do">Aufgaben</string>
@@ -309,20 +329,20 @@
<string name="editor_title">Editor</string>
<string name="editor_subtitle">Standardansicht • Standardbearbeitungsmodus</string>
<string name="show_line_numbers_title">Zeilennummern anzeigen</string>
<string name="show_line_numbers_description">
Zeilennummern im Editor anzeigen
</string>
<string name="show_line_numbers_description">Zeilennummern im Editor anzeigen</string>
<string name="markdown_lint_title">Markdown-Lint</string>
<string name="markdown_lint_description">
Markdown-Format und Syntax werden überprüft
</string>
<string name="markdown_lint_description">Markdown-Format und Syntax werden überprüft</string>
<string name="default_editor_view">Standard-Editoransicht</string>
<string name="reading_view">Leseansicht</string>
<string name="editing_view">Bearbeitungsansicht</string>
<string name="shareContent">
Flux ist eine moderne Android-Produktivitäts-App, die vollständig mit Compose entwickelt wurde.\n
Sie ermöglicht das Erstellen, Bearbeiten und Löschen von Workspaces mit Vorlagen wie Notizen, Gewohnheiten, Journal, To-Do, Ereignissen usw. sowie erweiterten Funktionen wie Suchen und Ersetzen, Notizvorlagen, lokalem Import von Bildern/Videos/Audio, Markdown, LaTeX-Formeln und Mermaid-Diagrammen.\n
Jetzt herunterladen und ausprobieren:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">Respaldar tus datos a un archivo</string>
<string name="Restore">Restaurar</string>
<string name="Restore_Description">Restaurar tus datos desde un archivo de respaldo</string>
<string name="reset">Restablecer</string>
<string name="backup_frequency">Frecuencia de copia de seguridad automática</string>
<string name="reset_database">Restablecer base de datos</string>
<string name="clear_app_data">Borrar todos los datos de la app</string>
<string name="never">Nunca</string>
<string name="daily">Diario</string>
<string name="weekly">Semanal</string>
<string name="monthly">Mensual</string>
<!-- Settings - About -->
<string name="About">Acerca de</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Repositorio de Github</string>
<string name="license">Licencia</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Guía de usuario</string>
<string name="user_guide_description">Lee la guía para aprovechar completamente la aplicación.</string>
<string name="privacy_policy_title">Política de privacidad</string>
<string name="privacy_policy_description">Política de privacidad de la aplicación.</string>
<string name="share_title">Compartir</string>
<string name="share_description">Comparte la aplicación con amigos y familiares.</string>
<string name="data_title">Datos</string>
<string name="data_subtitle">Copia de seguridad • Recuperación</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Contacto</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Error al importar la nota</string>
<string name="find">Buscar</string>
<string name="replace">Reemplazar</string>
<string name="record_audio">Grabar audio</string>
<string name="tap_to_start">Toca para comenzar</string>
<string name="retry">Reintentar</string>
<string name="permission_required">Por favor concede permiso de grabación.</string>
<!-- To-Do -->
<string name="To_Do">Tareas</string>
@@ -321,4 +341,12 @@
<string name="reading_view">Vista de lectura</string>
<string name="editing_view">Vista de edición</string>
<string name="shareContent">
Flux es una aplicación moderna de productividad para Android creada completamente con Compose.\n
Permite crear, editar y eliminar espacios de trabajo con plantillas como Notas, Hábitos, Diario, Tareas, Eventos, etc., junto con funciones avanzadas como buscar y reemplazar, plantillas de notas, importación local de imagen/video/audio, Markdown, fórmulas matemáticas LaTeX y diagramas Mermaid.\n
Descárgala y pruébala ahora:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">Sauvegarder vos données dans un fichier</string>
<string name="Restore">Restaurer</string>
<string name="Restore_Description">Restaurer vos données depuis un fichier de sauvegarde</string>
<string name="reset">Réinitialiser</string>
<string name="backup_frequency">Fréquence de sauvegarde automatique</string>
<string name="reset_database">Réinitialiser la base de données</string>
<string name="clear_app_data">Effacer toutes les données de lapplication</string>
<string name="never">Jamais</string>
<string name="daily">Quotidien</string>
<string name="weekly">Hebdomadaire</string>
<string name="monthly">Mensuel</string>
<!-- Settings - About -->
<string name="About">À propos</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Dépôt Github</string>
<string name="license">Licence</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Guide dutilisation</string>
<string name="user_guide_description">Lisez le guide pour utiliser pleinement lapplication.</string>
<string name="privacy_policy_title">Politique de confidentialité</string>
<string name="privacy_policy_description">Politique de confidentialité de lapplication.</string>
<string name="share_title">Partager</string>
<string name="share_description">Partagez lapplication avec vos amis et votre famille.</string>
<string name="data_title">Données</string>
<string name="data_subtitle">Sauvegarde • Récupération</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Contact</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Échec de l\'importation de la note</string>
<string name="find">Rechercher</string>
<string name="replace">Remplacer</string>
<string name="record_audio">Enregistrer laudio</string>
<string name="tap_to_start">Appuyez pour démarrer</string>
<string name="retry">Réessayer</string>
<string name="permission_required">Veuillez accorder lautorisation denregistrement.</string>
<!-- To-Do -->
<string name="To_Do">À faire</string>
@@ -324,4 +344,12 @@
<string name="reading_view">Vue de lecture</string>
<string name="editing_view">Vue d’édition</string>
<string name="shareContent">
Flux est une application moderne de productivité Android entièrement conçue avec Compose.\n
Elle permet de créer, modifier et supprimer des espaces de travail avec des modèles comme Notes, Habitudes, Journal, Tâches, Événements, etc., ainsi que des fonctionnalités avancées telles que rechercher/remplacer, modèles de notes, import local dimages/vidéos/audio, Markdown, formules LaTeX et diagrammes Mermaid.\n
Téléchargez-la dès maintenant :\n
- GitHub : https://github.com/chindaronit/Flux/releases\n
- F-Droid : https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">अपने डेटा का बैकअप फ़ाइल में लें</string>
<string name="Restore">पुनर्स्थापित करें</string>
<string name="Restore_Description">बैकअप फ़ाइल से अपना डेटा पुनर्स्थापित करें</string>
<string name="reset">रीसेट करें</string>
<string name="backup_frequency">स्वचालित बैकअप की आवृत्ति</string>
<string name="reset_database">डेटाबेस रीसेट करें</string>
<string name="clear_app_data">सभी ऐप डेटा साफ़ करें</string>
<string name="never">कभी नहीं</string>
<string name="daily">दैनिक</string>
<string name="weekly">साप्ताहिक</string>
<string name="monthly">मासिक</string>
<!-- Settings - About -->
<string name="About">अबाउट</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">गिटहब रिपोजिटरी</string>
<string name="license">लाइसेंस</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">उपयोगकर्ता मार्गदर्शिका</string>
<string name="user_guide_description">एप्लिकेशन का पूर्ण उपयोग करने के लिए मार्गदर्शिका पढ़ें।</string>
<string name="privacy_policy_title">गोपनीयता नीति</string>
<string name="privacy_policy_description">एप्लिकेशन की गोपनीयता नीति।</string>
<string name="share_title">साझा करें</string>
<string name="share_description">एप्लिकेशन को मित्रों और परिवार के साथ साझा करें।</string>
<string name="data_title">डेटा</string>
<string name="data_subtitle">बैकअप • रिकवरी</string>
<!-- Settings - Contact & Support -->
<string name="Contact">संपर्क करें</string>
@@ -190,6 +206,10 @@
<string name="import_failed">नोट आयात करने में विफल</string>
<string name="find">खोजें</string>
<string name="replace">बदलें</string>
<string name="record_audio">ऑडियो रिकॉर्ड करें</string>
<string name="tap_to_start">शुरू करने के लिए टैप करें</string>
<string name="retry">पुनः प्रयास करें</string>
<string name="permission_required">कृपया रिकॉर्डिंग की अनुमति दें।</string>
<!-- To-Do -->
<string name="To_Do">करने के कार्य</string>
@@ -324,4 +344,12 @@
<string name="reading_view">पठन दृश्य</string>
<string name="editing_view">संपादन दृश्य</string>
<string name="shareContent">
Flux एक आधुनिक Android प्रोडक्टिविटी ऐप है जिसे पूरी तरह Compose से बनाया गया है।\n
यह आपको Notes, Habits, Journal, To-Do, Events आदि जैसे टेम्पलेट्स के साथ वर्कस्पेस बनाने, संपादित करने और हटाने की सुविधा देता है, साथ ही find &amp; replace, note templates, लोकल इमेज/वीडियो/ऑडियो इम्पोर्ट, Markdown, LaTeX गणित सूत्र और Mermaid डायग्राम सपोर्ट जैसी उन्नत सुविधाएँ प्रदान करता है।\n
अभी डाउनलोड करें:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">Maak een back-up van je gegevens naar een bestand</string>
<string name="Restore">Herstellen</string>
<string name="Restore_Description">Herstel je gegevens van een back-upbestand</string>
<string name="reset">Resetten</string>
<string name="backup_frequency">Frequentie van automatische back-up</string>
<string name="reset_database">Database resetten</string>
<string name="clear_app_data">Alle appgegevens wissen</string>
<string name="never">Nooit</string>
<string name="daily">Dagelijks</string>
<string name="weekly">Wekelijks</string>
<string name="monthly">Maandelijks</string>
<!-- Settings - About -->
<string name="About">Over</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Github Repository</string>
<string name="license">Licentie</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Gebruikershandleiding</string>
<string name="user_guide_description">Lees de handleiding om de applicatie volledig te benutten.</string>
<string name="privacy_policy_title">Privacybeleid</string>
<string name="privacy_policy_description">Privacybeleid van de applicatie.</string>
<string name="share_title">Delen</string>
<string name="share_description">Deel de applicatie met vrienden en familie.</string>
<string name="data_title">Gegevens</string>
<string name="data_subtitle">Back-up • Herstel</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Contact</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Mislukt om notitie te importeren</string>
<string name="find">Zoeken</string>
<string name="replace">Vervangen</string>
<string name="record_audio">Audio opnemen</string>
<string name="tap_to_start">Tik om te starten</string>
<string name="retry">Opnieuw proberen</string>
<string name="permission_required">Geef alstublieft opname-toestemming.</string>
<!-- To-Do -->
<string name="To_Do">Te Doen</string>
@@ -323,4 +343,11 @@
<string name="reading_view">Leesweergave</string>
<string name="editing_view">Bewerkingsweergave</string>
<string name="shareContent">
Flux is een moderne Android-productiviteitsapp die volledig met Compose is gebouwd.\n
Hiermee kun je werkruimtes maken, bewerken en verwijderen met sjablonen zoals Notities, Gewoonten, Dagboek, To-Do, Evenementen, enz., samen met geavanceerde functies zoals zoeken en vervangen, notitiesjablonen, lokale import van afbeeldingen/video/audio, Markdown, LaTeX-formules en Mermaid-diagrammen.\n
Download en probeer het nu:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">Fazer backup dos seus dados em um arquivo</string>
<string name="Restore">Restaurar</string>
<string name="Restore_Description">Restaurar seus dados a partir de um arquivo de backup</string>
<string name="reset">Redefinir</string>
<string name="backup_frequency">Frequência do backup automático</string>
<string name="reset_database">Redefinir banco de dados</string>
<string name="clear_app_data">Limpar todos os dados do app</string>
<string name="never">Nunca</string>
<string name="daily">Diário</string>
<string name="weekly">Semanal</string>
<string name="monthly">Mensal</string>
<!-- Settings - About -->
<string name="About">Sobre</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Repositório no GitHub</string>
<string name="license">Licença</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Guia do usuário</string>
<string name="user_guide_description">Leia o guia para utilizar completamente o aplicativo.</string>
<string name="privacy_policy_title">Política de privacidade</string>
<string name="privacy_policy_description">Política de privacidade do aplicativo.</string>
<string name="share_title">Compartilhar</string>
<string name="share_description">Compartilhe o aplicativo com amigos e familiares.</string>
<string name="data_title">Dados</string>
<string name="data_subtitle">Backup • Recuperação</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Contato</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Falha ao importar nota</string>
<string name="find">Pesquisar</string>
<string name="replace">Substituir</string>
<string name="record_audio">Gravar áudio</string>
<string name="tap_to_start">Toque para começar</string>
<string name="retry">Tentar novamente</string>
<string name="permission_required">Por favor conceda permissão de gravação.</string>
<!-- To-Do -->
<string name="To_Do">Tarefas</string>
@@ -324,4 +344,12 @@
<string name="reading_view">Visualização de leitura</string>
<string name="editing_view">Visualização de edição</string>
<string name="shareContent">
Flux é um aplicativo moderno de produtividade Android construído inteiramente com Compose.\n
Ele permite criar, editar e excluir espaços de trabalho com modelos como Notas, Hábitos, Diário, Tarefas, Eventos etc., além de recursos avançados como localizar e substituir, modelos de notas, importação local de imagem/vídeo/áudio, Markdown, fórmulas LaTeX e diagramas Mermaid.\n
Baixe e experimente agora:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
@@ -120,6 +120,14 @@
<string name="Backup_Description">Сохранить данные в файл</string>
<string name="Restore">Восстановить</string>
<string name="Restore_Description">Восстановить данные из резервной копии</string>
<string name="reset">Сбросить</string>
<string name="backup_frequency">Частота автоматического резервного копирования</string>
<string name="reset_database">Сбросить базу данных</string>
<string name="clear_app_data">Очистить все данные приложения</string>
<string name="never">Никогда</string>
<string name="daily">Ежедневно</string>
<string name="weekly">Еженедельно</string>
<string name="monthly">Ежемесячно</string>
<!-- Settings - About -->
<string name="About">О приложении</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Репозиторий GitHub</string>
<string name="license">Лицензия</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">Руководство пользователя</string>
<string name="user_guide_description">Прочитайте руководство, чтобы полностью использовать приложение.</string>
<string name="privacy_policy_title">Политика конфиденциальности</string>
<string name="privacy_policy_description">Политика конфиденциальности приложения.</string>
<string name="share_title">Поделиться</string>
<string name="share_description">Поделитесь приложением с друзьями и семьёй.</string>
<string name="data_title">Данные</string>
<string name="data_subtitle">Резервное копирование • Восстановление</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Контакт</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Ошибка при импорте заметки</string>
<string name="find">Найти</string>
<string name="replace">Заменить</string>
<string name="record_audio">Записать аудио</string>
<string name="tap_to_start">Нажмите, чтобы начать</string>
<string name="retry">Повторить</string>
<string name="permission_required">Пожалуйста, предоставьте разрешение на запись.</string>
<!-- To-Do -->
<string name="To_Do">Задачи</string>
@@ -323,4 +343,12 @@
<string name="reading_view">Режим чтения</string>
<string name="editing_view">Режим редактирования</string>
<string name="shareContent">
Flux — это современное Android-приложение для продуктивности, полностью созданное с использованием Compose.\n
Оно позволяет создавать, редактировать и удалять рабочие пространства с шаблонами, такими как Заметки, Привычки, Дневник, Задачи, События и др., а также поддерживает расширенные функции: поиск и замена, шаблоны заметок, локальный импорт изображений/видео/аудио, Markdown, формулы LaTeX и диаграммы Mermaid.\n
Скачайте и попробуйте сейчас:\n
- GitHub: https://github.com/chindaronit/Flux/releases\n
- F-Droid: https://f-droid.org/packages/com.flux/
</string>
</resources>
+29 -1
View File
@@ -120,6 +120,14 @@
<string name="Backup_Description">Backup your data to a file</string>
<string name="Restore">Restore</string>
<string name="Restore_Description">Restore your data from backup file</string>
<string name="reset">Reset</string>
<string name="backup_frequency">Frequency of automatic backup</string>
<string name="reset_database">Reset Database</string>
<string name="clear_app_data">Clear all app data</string>
<string name="never">Never</string>
<string name="daily">Daily</string>
<string name="weekly">Weekly</string>
<string name="monthly">Monthly</string>
<!-- Settings - About -->
<string name="About">About</string>
@@ -132,6 +140,14 @@
<string name="Github_Repository">Github Repository</string>
<string name="license">License</string>
<string name="license_gpl3_only">GPL-3.0 Only</string>
<string name="user_guide_title">User Guide</string>
<string name="user_guide_description">Read the guide to fully utilize the application.</string>
<string name="privacy_policy_title">Privacy Policy</string>
<string name="privacy_policy_description">Privacy policy of the application.</string>
<string name="share_title">Share</string>
<string name="share_description">Share the application with friends and family.</string>
<string name="data_title">Data</string>
<string name="data_subtitle">Backup • Recovery</string>
<!-- Settings - Contact & Support -->
<string name="Contact">Contact</string>
@@ -190,6 +206,10 @@
<string name="import_failed">Failed to import note</string>
<string name="find">Find</string>
<string name="replace">Replace</string>
<string name="record_audio">Record Audio</string>
<string name="tap_to_start">Tap to start</string>
<string name="retry">Retry</string>
<string name="permission_required">Please provide recording permission.</string>
<!-- To-Do -->
<string name="To_Do">To-Do</string>
@@ -304,7 +324,6 @@
<string name="Space_Content_Desc">space</string>
<string name="Edit_Space_Content_Desc">Edit-space</string>
<!-- Editor -->
<string name="editor_title">Editor</string>
<string name="editor_subtitle">Default View • Default Editing Mode</string>
@@ -329,4 +348,13 @@
<string name="editing_view">Editing View</string>
<string name="shareContent">
Flux is a modern Android productivity app built entirely with Compose.\n
It allows you to create, edit, and delete workspace which has various templates such as Notes, Habits, Journal, To-Do, Events, etc. along with advanced functionalities such as find and replace, note templates, local image/video/audio import, Markdown, LaTeX math formulas, and Mermaid diagram support.\n
Download and try it now:\n
- [GitHub] https://github.com/chindaronit/Flux/releases\n
- [F-droid] https://f-droid.org/packages/com.flux/
</string>
</resources>
+4
View File
@@ -24,6 +24,8 @@ hilt = "2.57.2"
hiltNavigationCompose = "1.3.0"
browser = "1.9.0"
documentfile = "1.1.0"
workRuntimeKtx = "2.11.1"
foundationLayout = "1.10.3"
[libraries]
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
@@ -66,6 +68,8 @@ commonmark-ext-heading-anchor = { group = "org.commonmark", name = "commonmark-e
commonmark-ext-yaml-front-matter = { group = "org.commonmark", name = "commonmark-ext-yaml-front-matter", version.ref = "commonmark" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+4
View File
@@ -0,0 +1,4 @@
* Audio recorder added to notes and journal.
* Privacy Policy and User guide added in About.
* Removal of redundant editor options in Customize Screen.
* Automatic Backup manager added in Flux.
+1
View File
@@ -0,0 +1 @@
Remove upgrade room crash bug.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB