-
Notifications
You must be signed in to change notification settings - Fork 0
feat: markdown rendering #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e8b4692
005ba17
939a5ec
6501fb8
830db3f
d982cbc
12f39fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,14 +3,16 @@ package io.github.smiling_pixel | |||||||
| import io.github.smiling_pixel.model.DiaryEntry | ||||||||
| import io.github.smiling_pixel.client.WeatherClient | ||||||||
| import io.github.smiling_pixel.model.Location | ||||||||
| import com.mikepenz.markdown.m3.Markdown | ||||||||
| import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl | ||||||||
| import kotlin.time.Clock | ||||||||
| import kotlin.time.Instant | ||||||||
|
||||||||
| import kotlin.time.Instant | |
| import kotlinx.datetime.Instant |
SmilingPixel marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Markdown component is missing a modifier parameter. In edit mode, the corresponding OutlinedTextField uses Modifier.fillMaxSize() to fill the available space. For consistency and to ensure the Markdown content is properly laid out, consider adding a modifier parameter such as modifier = Modifier.fillMaxWidth() to ensure the Markdown content has proper width constraints.
| Markdown( | |
| Markdown( | |
| modifier = Modifier.fillMaxWidth(), |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Markdown component doesn't specify a modifier, which means it won't have any layout constraints applied. Consider adding a modifier parameter to control the layout behavior, especially since this is in a Column that could have scrolling or size requirements.
| content = entry.content, | |
| content = entry.content, | |
| modifier = Modifier.fillMaxWidth(), |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package io.github.smiling_pixel | ||
|
|
||
| import coil3.ImageLoader | ||
| import coil3.PlatformContext | ||
| import coil3.network.ktor3.KtorNetworkFetcherFactory | ||
| import coil3.request.crossfade | ||
| import io.github.smiling_pixel.filesystem.LocalFileFetcher | ||
| import io.github.smiling_pixel.filesystem.fileManager | ||
|
|
||
| fun getAsyncImageLoader(context: PlatformContext): ImageLoader { | ||
| return ImageLoader.Builder(context) | ||
| .components { | ||
| add(LocalFileFetcher.Factory(fileManager)) // use localfile scheme, e.g., `localfile://myimage.jpg`, see LocalFileFetcher implementation for details | ||
| add(KtorNetworkFetcherFactory()) | ||
| } | ||
| .crossfade(true) | ||
| .build() | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -40,7 +40,7 @@ import androidx.compose.ui.graphics.Color | |||||
| import androidx.compose.ui.unit.dp | ||||||
| import io.github.smiling_pixel.filesystem.FileRepository | ||||||
| import io.github.smiling_pixel.model.FileMetadata | ||||||
| import kotlinx.datetime.Instant | ||||||
| import kotlin.time.Instant | ||||||
|
||||||
| import kotlin.time.Instant | |
| import kotlinx.datetime.Instant |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package io.github.smiling_pixel.filesystem | ||
|
|
||
| import coil3.ImageLoader | ||
| import coil3.Uri | ||
| import coil3.decode.DataSource | ||
| import coil3.decode.ImageSource | ||
| import coil3.fetch.Fetcher | ||
| import coil3.fetch.FetchResult | ||
| import coil3.fetch.SourceFetchResult | ||
| import coil3.request.Options | ||
| import okio.Buffer | ||
| import okio.FileSystem | ||
| import okio.Path | ||
| import okio.IOException | ||
|
|
||
| class LocalFileFetcher( | ||
| private val fileName: String, | ||
| private val fileManager: FileManager | ||
| ) : Fetcher { | ||
|
|
||
| override suspend fun fetch(): FetchResult? { | ||
| val bytes = fileManager.read(fileName) ?: run { | ||
| val errorMessage = "LocalFileFetcher: File not found: $fileName" | ||
| println(errorMessage) | ||
| // Returning null here would let Coil try other fetchers, but since we handle | ||
| // the 'localfile' scheme, no other fetcher is expected to succeed. | ||
| // Throwing an exception provides a more informative error result. | ||
| throw IOException(errorMessage) | ||
| } | ||
| val buffer = Buffer().write(bytes) | ||
|
|
||
| return SourceFetchResult( | ||
| source = ImageSource(buffer, EmptyFileSystem), | ||
| mimeType = null, | ||
| dataSource = DataSource.DISK | ||
| ) | ||
| } | ||
|
|
||
SmilingPixel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Factory for creating [LocalFileFetcher] instances for URIs with the `localfile` scheme. | ||
| * | ||
| * Expected formats include: | ||
| * - `localfile:image.jpg` | ||
| * - `localfile:/image.jpg` | ||
| * - `localfile:///image.jpg` | ||
| * | ||
| * In all cases, [Uri.path] is used and any leading '/' characters are trimmed before | ||
| * being passed to [FileManager]. Only the `localfile` scheme is recognized here. | ||
| */ | ||
| class Factory(private val fileManager: FileManager) : Fetcher.Factory<Uri> { | ||
| override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? { | ||
| if (data.scheme == "localfile") { | ||
| // data.path might start with /, e.g. /image.jpg | ||
| val fileName = data.path?.trimStart('/') ?: run { | ||
| println("LocalFileFetcher: Empty path in URI: $data") | ||
| return null | ||
| } | ||
|
|
||
| // Reject potentially unsafe paths to prevent directory traversal. | ||
| // This ensures inputs like "../secret.png" or "a/../../etc/passwd" are not used. | ||
| if (fileName.isEmpty() || fileName.contains("..")) { | ||
| println("LocalFileFetcher: Rejected potentially unsafe or empty path: $fileName") | ||
| return null | ||
| } | ||
| return LocalFileFetcher(fileName, fileManager) | ||
| } | ||
| return null | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A dummy [FileSystem] implementation used for [ImageSource] when the data is already buffered in memory. | ||
| * | ||
| * Since we load the file content into an Okio [Buffer] using [FileManager] and pass that buffer to [ImageSource], | ||
| * Coil does not need to read from the file system directly for this source. | ||
| * This implementation safely returns null or throws strict exceptions to ensure no unintended file system usage occurs. | ||
| */ | ||
| object EmptyFileSystem : FileSystem() { | ||
| override fun canonicalize(path: Path) = path | ||
| override fun metadataOrNull(path: Path) = null | ||
| override fun list(dir: Path) = throw IOException("Not supported") | ||
| override fun listOrNull(dir: Path): List<Path>? = null | ||
| override fun openReadOnly(file: Path) = throw IOException("Not supported") | ||
| override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean) = throw IOException("Not supported") | ||
| override fun source(file: Path) = throw IOException("Not supported") | ||
| override fun sink(file: Path, mustCreate: Boolean) = throw IOException("Not supported") | ||
| override fun appendingSink(file: Path, mustExist: Boolean) = throw IOException("Not supported") | ||
| override fun createDirectory(dir: Path, mustCreate: Boolean) = throw IOException("Not supported") | ||
| override fun atomicMove(source: Path, target: Path) = throw IOException("Not supported") | ||
| override fun delete(path: Path, mustExist: Boolean) = throw IOException("Not supported") | ||
| override fun createSymlink(source: Path, target: Path) = throw IOException("Not supported") | ||
|
Comment on lines
+82
to
+92
|
||
| } | ||
SmilingPixel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setSingletonImageLoaderFactory is called inside the App composable function without any remember or LaunchedEffect wrapper. This means it will be called on every recomposition of the App composable, which is inefficient. Consider wrapping it in a LaunchedEffect with an empty dependency list to ensure it's only called once, or moving it outside the composable to an initialization location.