Coder Social home page Coder Social logo

vinceglb / filekit Goto Github PK

View Code? Open in Web Editor NEW
260.0 5.0 11.0 583 KB

Pick and save Files, Medias and Folder for Kotlin Multiplatform / KMP and Compose Multiplatform / CMP

Home Page: https://vinceglb.github.io/FileKit/

License: MIT License

Kotlin 100.00%
compose compose-multiplatform kotlin kotlin-android kotlin-ios kotlin-js kotlin-jvm kotlin-macos kotlin-multiplatform kotlin-wasm

filekit's Introduction

FileKit for Kotlin Multiplatform and Compose Multiplatform

FileKit

Files, Medias, Folder Picker and File saver library for Kotlin Multiplatform and Compose Multiplatform

FileKit Kotlin Maven Version Badge Android Badge iOS Badge JVM Badge JS

FileKit is a library that allows you to pick and save files in a simple way. On each platform, it uses the native file picker API to provide a consistent experience.

πŸš€ Quick Start

Pick a file, a directory or save a file in common code:

// Pick a file
val file = FileKit.pickFile()

// Pick a directory
val directory = FileKit.pickDirectory()

// Save a file
val file = FileKit.saveFile(
    extension = "txt",
    bytes = "Hello, World!".encodeToByteArray()
)

Get file information in common code:

val filePath = file?.path
val fileName = file?.name
val bytes = file?.readBytes()

Compose Multiplatform integration made simple:

// Pick files from Compose
val launcher = rememberFilePickerLauncher(PickerMode.Multiple) { files ->
    // Handle picked files
}

// Use the pickerLauncher
Button(onClick = { launcher.launch() }) {
    Text("Pick files")
}

FileKit Preview

πŸ“¦ Installation

repositories {
    mavenCentral()
}

dependencies {
    // Enables FileKit without Compose dependencies
    implementation("io.github.vinceglb:filekit-core:0.6.3")

    // Enables FileKit with Composable utilities
    implementation("io.github.vinceglb:filekit-compose:0.6.3")
}

⚑ Initialization

Using FileKit Core methods on Android requires an initialization:

  • FileKit.pickFile()
  • FileKit.pickDirectory()
  • FileKit.saveFile()

In this case, only if using Android, you need to initialize FileKit in your ComponentActivity:

// MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate() {
        super.onCreate()
        FileKit.init(this)
    }
}

In all other cases, you can use FileKit without initialization.

πŸ“„ File Picker

Picker types

You can pick different types of files with PickerType:

  • Image: Pick an image file.
  • Video: Pick a video file.
  • ImageAndVideo: Pick an image or a video file.
  • File: Pick any file. It is the default type. It's possible to specify a list of extensions.
val imageType = PickerType.Image
val videoType = PickerType.Video
val imageAndVideoType = PickerType.ImageAndVideo
val fileType = PickerType.File(extensions = listOf("pdf", "docx"))

Picker modes

You can pick files in different modes with PickerMode. The mode will change the output type. Single is the default mode.

val singleMode = PickerMode.Single
val multipleMode = PickerMode.Multiple

Launch the picker

You can launch the picker with FileKit.pickFile or rememberFilePickerLauncher:

// FileKit Core
val file = FileKit.pickFile(
    type = PickerType.Image,
    mode = PickerMode.Single,
    title = "Pick an image",
    initialDirectory = "/custom/initial/path"
)

// FileKit Compose
val launcher = rememberFilePickerLauncher(
    type = PickerSelectionType.ImageAndVideo,
    mode = PickerSelectionMode.Multiple,
    title = "Pick a media",
    initialDirectory = "/custom/initial/path"
) { files ->
    // Handle the picked files
}
launcher.launch()

πŸ“ Directory Picker

You can pick a directory with FileKit.pickDirectory or rememberDirectoryPickerLauncher:

// FileKit Core
val directory = FileKit.pickDirectory(
    title = "Pick a directory",
    initialDirectory = "/custom/initial/path"
)

// FileKit Compose
val launcher = rememberDirectoryPickerLauncher(
    title = "Pick a directory",
    initialDirectory = "/custom/initial/path"
) { directory ->
    // Handle the picked directory
}
launcher.launch()

The directory picker is available on all platforms, expect for WASM / JS. To check if the directory picker is available from the common code, you can use FileKit.isDirectoryPickerSupported().

val directoryModeSupported = FileKit.isDirectoryPickerSupported()

πŸ’Ύ Save File Picker

You can save a file with FileKit.saveFile or rememberFileSaverLauncher:

// FileKit Core
val file = FileKit.saveFile(
    baseName = "myTextFile",
    extension = "txt",
    initialDirectory = "/custom/initial/path",
    bytes = "Hello, World!".encodeToByteArray()
)

// FileKit Compose
val launcher = rememberFileSaverLauncher() { file ->
    // Handle the saved file
}
launcher.launch(
    baseName = "myTextFile",
    extension = "txt",
    initialDirectory = "/custom/initial/path",
    bytes = "Hello, World!".encodeToByteArray()
)

Optional bytes argument

Bytes argument is optional. If you don't provide it, the file will be empty. This feature is available on Android, iOS, macOS and JVM. It is not available on WASM / JS.

To check if it's possible to save a file without bytes from the common code, you can use:

val isSupported: Boolean = FileKit.isSaveFileWithoutBytesSupported()

πŸ§‘β€πŸ’» PlatformFile and PlatformDirectory

The PlatformFile and PlatformDirectory classes are wrappers around the platform file system. It allows you to get the file name, path and read the file content in common code.

val platformFile: PlatformFile = ...

val filePath: String? = platformFile.path
val fileName: String = platformFile.name            // Base name with extension
val baseName: String = platformFile.baseName
val extension: String = platformFile.extension
val size: Long = platformFile.getSize()
val bytes: ByteArray = platformFile.readBytes()     // suspend function

val platformDirectory: PlatformDirectory = ...
val directoryPath: String? = platformDirectory.path

On each platform, you can get the original platform file:

// Android
val uri: Uri = platformFile.uri
val uri: Uri = platformDirectory.uri

// iOS / macOS
val nsUrl: NSURL = platformFile.nsUrl
val nsUrl: NSURL = platformDirectory.nsUrl

// JVM
val file: java.io.File = platformFile.file
val file: java.io.File = platformDirectory.file

// WASM / JS
val file: org.w3c.files.File = platformFile.file
val file: org.w3c.files.File = // PlatformDirectory not supported on WASM / JS

🌱 Sample projects

You can find 2 sample projects in the samples directory:

  • sample-core: A Kotlin Multiplatform project using FileKit in a shared viewModel targeting Android, JVM, WASM, JS, iOS Swift, macOS Swift and iOS Compose.
  • sample-compose: A Compose Multiplatform project using FileKit in a Composable targeting Android, iOS, JVM, WASM,

✨ Behind the scene

FileKit uses the native file picker API on each platform:

  • On Android, it uses PickVisualMedia, OpenDocument and OpenDocumentTree contracts.
  • On iOS, it uses both UIDocumentPickerViewController and PHPickerViewController APIs.
  • On macOS, it uses the NSOpenPanel API.
  • On JVM, it uses JNA to access the file system on Windows and macOS and Awt FileDialog on Linux.
  • On WASM / JS, it uses the input element with the file type.

Also, FileKit uses the bear minimum of dependencies to be as lightweight as possible.

FileKit Core uses the following libraries:

FileKit Compose uses the following libraries:

😎 Credits

FileKit is inspired by the following libraries:


Made with ❀️ by Vince

filekit's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

filekit's Issues

ReadBytes() crashes on iOS sometimes

readBytes() method crashes on iOS

Stack Trace:

Fatal Exception: kotlin.IllegalStateException
Failed to read data from file:///private/var/mobile/Containers/Data/Application/D917AB1D-00E7-4BDF-B7CD-2E12E6CDF147/tmp/.com.apple.Foundation.NSItemProvider.DJVXPj/IMG_0020.jpeg

Fatal Exception: kotlin.IllegalStateException
0  Medial                         0x2b6aa2c kfun:io.github.vinceglb.filekit.core.PlatformFile.readBytes$lambda$0#internal + 38 (PlatformFile.apple.kt:38)
1  Medial                         0x2b6b1e4 kfun:io.github.vinceglb.filekit.core.PlatformFile.$readBytes$lambda$0$FUNCTION_REFERENCE$0.invoke#internal + 35 (PlatformFile.apple.kt:35)
2  Medial                         0xdcdcb0 kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 1 ([K][Suspend]Functions:1)
3  Medial                         0xc8f140 kfun:kotlin.coroutines.intrinsics.object-4.invokeSuspend#internal + 72 (IntrinsicsNative.kt:72)
4  Medial                         0xdcd5cc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 50 (ContinuationImpl.kt:50)
5  Medial                         0xc8b8ac kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 30 (ContinuationImpl.kt:30)
6  Medial                         0xdcd6ac kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 26 (Continuation.kt:26)
7  Medial                         0xed5d34 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 45 (Continuation.kt:45)
8  Medial                         0xf049cc kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 10 (Runnable.kt:10)
9  Medial                         0xed8a08 kfun:kotlinx.coroutines.internal.LimitedDispatcher.Worker.run#internal + 113 (LimitedDispatcher.kt:113)
10 Medial                         0xf049cc kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 10 (Runnable.kt:10)
11 Medial                         0xefbce0 kfun:kotlinx.coroutines.MultiWorkerDispatcher.$workerRunLoop$lambda$2COROUTINE$0.invokeSuspend#internal + 113 (MultithreadedDispatchers.kt:113)
12 Medial                         0xdcd5cc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 50 (ContinuationImpl.kt:50)
13 Medial                         0xc8b8ac kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 30 (ContinuationImpl.kt:30)
14 Medial                         0xdcd6ac kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 26 (Continuation.kt:26)
15 Medial                         0xed5d34 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 45 (Continuation.kt:45)
16 Medial                         0xf049cc kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 10 (Runnable.kt:10)
17 Medial                         0xe5bd58 kfun:kotlinx.coroutines.EventLoopImplBase#processNextEvent(){}kotlin.Long + 263 (EventLoop.common.kt:263)
18 Medial                         0xf04718 kfun:kotlinx.coroutines.EventLoop#processNextEvent(){}kotlin.Long-trampoline + 49 (EventLoop.common.kt:49)
19 Medial                         0xef4ff0 kfun:kotlinx.coroutines.BlockingCoroutine.joinBlocking#internal + 129 (Builders.kt:129)
20 Medial                         0xef3e54 kfun:kotlinx.coroutines#runBlocking(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0Β§<kotlin.Any?>}0:0 + 68 (Builders.kt:68)
21 Medial                         0xef4034 kfun:kotlinx.coroutines#runBlocking$default(kotlin.coroutines.CoroutineContext?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>;kotlin.Int){0Β§<kotlin.Any?>}0:0 + 45 (Builders.kt:45)
22 Medial                         0xef9db0 kfun:kotlinx.coroutines.MultiWorkerDispatcher.workerRunLoop#internal + 99 (MultithreadedDispatchers.kt:99)
23 Medial                         0xefb12c kfun:kotlinx.coroutines.MultiWorkerDispatcher.<init>$lambda$1$lambda$0#internal + 86 (MultithreadedDispatchers.kt:86)
24 Medial                         0xefc428 kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$5.invoke#internal + 86 (MultithreadedDispatchers.kt:86)
25 Medial                         0xefc4f8 kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$5.$<bridge-UNN>invoke(){}#internal + 86 (MultithreadedDispatchers.kt:86)
26 Medial                         0xdc9f68 kfun:kotlin.Function0#invoke(){}1:0-trampoline + 1 ([K][Suspend]Functions:1)
27 Medial                         0xc98fcc WorkerLaunchpad + 106 (Internal.kt:106)
28 Medial                         0xe2b684 Worker::processQueueElement(bool)
29 Medial                         0xe2aac4 (anonymous namespace)::workerRoutine(void*)
30 libsystem_pthread.dylib        0x606c _pthread_start
31 libsystem_pthread.dylib        0x10d8 thread_start

[win] Support modal dialogs (hwndOwner)

Hi there, very useful library!

It would be great to be able to open the picker dialogs modally on Windows. This is important to tie the lifecycle and visibility of the picker window to its parent window, e.g. so that closing the parent window closes the picker. I think this should probably be the default behaviour.

It looks like this could be achieved by passing an optional window handle into Picker.pickFile, and then passing that down into WindowsFileChooser so it ends up as hwndOwner?

This could tie in neatly with the Compose library as an integration with FrameWindowScope, e.g. fun FrameWindowScope.rememberFilePickerLauncher(...) that opens modally by default?

IOS

val launcherPicker = rememberFilePickerLauncher(
type = PickerType.Video,
mode = PickerMode.Single,
title = "Pick a media",
) { files ->
println("files is ${files?.path}")
}

files is null why

Add Xdg Desktop File Chooser Portal for Linux

Currently AWT file chooser causes crashes on Kde systems (including my own) JetBrains/compose-multiplatform#1279

As an alternative Xdg Desktop Portals can be used
https://flatpak.github.io/xdg-desktop-portal/docs/reasons-to-use-portals.html
https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html

As far as I'm aware all modern Desktop Environments and distributions support portals and have them preinstalled
This allows to use native file picker for system specific environment (Kde, Gnome or any other implementation that's installed on the system)

Not localized on macOS (JVM)

Thanks for making this lib!
I like its API very much.

I noticed that on macOS (JVM) the FilePicker as well as the DirectoryPicker are not localized. They are always presented in English. On Windows the pickers are presented in the systems default language.

This is the same problem I currently encounter with compose-multiplatform-file-picker.

readByte() crashes when running application on a real device

When I tried to call PlatformFile#readByte() function, the app crashes.
The file picker functions are working perfectly on both emulators and real devices.
The readByte() function is working perfectly on emulators.

Xcode v.: Version 16.0 beta 2
emulator ipad os v.: 18.0
real device ipad os v.: 18.0

Encountered an issue while using the Save File Picker

Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Already resumed, but proposed with update PlatformFile(file=C:\Users\Acker\Desktop\myTextFile.txt)
	at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError(CancellableContinuationImpl.kt:551)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:516)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:489)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:364)
	at io.github.vinceglb.filekit.core.platform.awt.AwtFileSaver.saveFile$lambda$2$handleResult(AwtFileSaver.kt:27)
	at io.github.vinceglb.filekit.core.platform.awt.AwtFileSaver.access$saveFile$lambda$2$handleResult(AwtFileSaver.kt:12)
	at io.github.vinceglb.filekit.core.platform.awt.AwtFileSaver$saveFile$2$dialog$2.setVisible(AwtFileSaver.kt:45)
	at io.github.vinceglb.filekit.core.platform.awt.AwtFileSaver.saveFile(AwtFileSaver.kt:57)
	at io.github.vinceglb.filekit.core.FileKit$saveFile$2.invokeSuspend(FileKit.jvm.kt:76)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.scene.ComposeContainer$DesktopCoroutineExceptionHandler@6f20c911, androidx.compose.runtime.BroadcastFrameClock@703f84f, StandaloneCoroutine{Cancelling}@6c45d059, FlushCoroutineDispatcher@1780e81a]
       fun handleResult(value: Boolean, files: Array<File>?) {
          if (value) {
              val file = files?.firstOrNull()?.let { file ->
                  // Write bytes to file, or create a new file
                  bytes?.let { file.writeBytes(bytes) } ?: file.createNewFile()
                  PlatformFile(file)
              }
              continuation.resume(file)
          } else {
              continuation.resume(null)
          }
      }

      // Handle parentWindow: Dialog, Frame, or null
      val dialog = when (parentWindow) {
          is Dialog -> object : FileDialog(parentWindow, "Save dialog", SAVE) {
              override fun setVisible(value: Boolean) {
                  super.setVisible(value)
                  handleResult(value, files)
              }
          }

          else -> object : FileDialog(parentWindow as? Frame, "Save dialog", SAVE) {
              override fun setVisible(value: Boolean) {
                  super.setVisible(value)
                  handleResult(value, files)  // <-- the function was called twice, with the value `false` the first time and `true` the second time.
              }
          }
      }

[win] Bug: File extension filters persist across invocations

JnaFileChooser.setup() adds filters each time it's run, but the file chooser is setup as a singleton in PlatformFilePicker.

Since addFilter is an append, that means that each subsequent invocation shows filters from previous invocations.

Here's an example of opening an Image picker repeatedly -- each time the picker was opened previously, a filter is added.
Screenshot 2024-05-13 112915

This is likely also an issue with title and initialDirectory, which are set in a similar way.

Android Preview does not function

When using rememberFilePickerLauncher, the previewer in the androidMain source set stops functioning because of a ClassCastException. During preview rendering, the LocalContext.current returns a mocked BridgeContext and not a ComponentActivity

Stack Trace

java.lang.ClassCastException: class com.android.layoutlib.bridge.android.BridgeContext cannot be cast to class androidx.activity.ComponentActivity (com.android.layoutlib.bridge.android.BridgeContext is in unnamed module of loader com.intellij.ide.plugins.cl.PluginClassLoader @5b4286a7; androidx.activity.ComponentActivity is in unnamed module of loader org.jetbrains.android.uipreview.StudioModuleClassLoader @57ee5812) Β Β at io.github.vinceglb.filekit.compose.FileKitCompose_androidKt.InitFileKit(FileKitCompose.android.kt:11) Β Β at io.github.vinceglb.filekit.compose.FileKitComposeKt.rememberFilePickerLauncher(FileKitCompose.kt:26)

Unable to locate JNA native support library

Hello, i'm trying to migrate from https://github.com/Wavesonics/compose-multiplatform-file-picker to new library and faced crash.

Exception in thread "AWT-EventQueue-0 @coroutine#9527" java.lang.UnsatisfiedLinkError: Unable to locate JNA native support library
	at com.sun.jna.Native.loadNativeDispatchLibrary(Native.java:1014)
	at com.sun.jna.Native.<clinit>(Native.java:221)
	at io.github.vinceglb.filekit.core.platform.mac.foundation.Foundation.<clinit>(Foundation.kt:25)
	at io.github.vinceglb.filekit.core.platform.mac.foundation.Foundation$NSAutoreleasePool.<init>(Foundation.kt:563)
	at io.github.vinceglb.filekit.core.platform.mac.MacOSFilePicker.callNativeMacOSPicker(MacOSFilePicker.kt:57)
	at io.github.vinceglb.filekit.core.platform.mac.MacOSFilePicker.pickFile(MacOSFilePicker.kt:16)
	at io.github.vinceglb.filekit.core.FileKit$pickFile$2.invokeSuspend(FileKit.jvm.kt:28)

The library uses inside intellig idea plugin to choose file from system.

https://github.com/ComposeGears/Valkyrie/blob/95388a9fc803854274284de260be7df496df4783/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/conversion/ConversionScreen.kt#L89

Migrated version:

 val filePicker = rememberFilePickerLauncher(
        type = PickerType.File(listOf("svg", "xml")),
        mode = PickerMode.Single,
        initialDirectory = settings.initialDirectory,
        onResult = { file ->
            file?.let {
                onSelectFile(it.file)
            }
        }
    )

Library version: 0.6.1
Machine: Mackbook Pro M3

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.