$folder
A directory the user picked, remembered across restarts, and the files in it.
val library = $folder("library")
@if (library.granted) {
<Audio src={library.file("book/03.mp3")} background />
<Document src={library.page("book/03.html")} />
} else {
<Button onClick={() => library.choose()}>Choose your library</Button>
} Why this exists
static/ is what the developer shipped. A folder is what the user pointed at,
and its contents are unknown at build time: a downloaded book, a music library, an
export. An app needs somewhere to put those files and a way to hand them to <Audio>, <Image>, <Video> and <Document>.
No permission is involved
The user grants one directory through the system folder picker, not the storage volume.
No <uses-permission> entry is added and nothing is asked with $permission. The grant survives a restart. The key, "library" here, names which grant; an app may hold several, each remembered
separately. The set of folders an app has is the set of $folder(…) calls in
its source.
Members
granted— a folder is chosen and the grant still holds (state)name— the chosen folder's display name (state)choose()— open the system folder picker; does nothing with no activity on screenfile(path)— acontent://address for anysrcthat takes one. It does not check the file existspage(path)— an address<Document>can resolve siblings against, so a page's stylesheet and images load from the folderlist(path)— the names directly inside a path, sorted; with no argument, inside the folder itselfexists(path)— whether a path is there; asks the providerforget()— give the grant back; the files are untouchedrefresh()— re-read the grant; done for you when the app resumesdownload(url, to = path)— fetch into the folder, and watch it arrive
file and page are different on purpose. A content:// document cannot resolve the stylesheet beside it, so a <Document> shown from file() would arrive as unstyled
text. page() serves the folder so relative links work.
download
val job = library.download("https://example.com/book.zip", to = "book/book.zip")
job.progress // 0..1, or -1 when the server did not say how big the file is
job.bytes // written so far
job.done
job.failed // the failure as a message, null while it is going fine
job.cancel() // what has already been written stays written The handle is a live object rather than a call you wait on, because a download can
outlive the screen that asked for it. Calling download again with the same
destination returns the same handle, so reading it in markup starts one download, not one
per redraw. Missing directories in to are created.
file() returns an empty
string and list() an empty list. A screen can be written for the granted
case and shown behind library.granted.Try it
import $lib.components.ui.Button
val library = $folder("library")
<Column class="gap-8">
@if (library.granted) {
<Text>{library.name}</Text>
<Button onClick={() => library.forget()}>Forget</Button>
} else {
<Button onClick={() => library.choose()}>Choose your library</Button>
}
</Column>granted is state, so the screen changes by itself once a folder is chosen. The key names which grant; it is not a path.
See Also
- <Audio> and <Video> — play from
file() - <Document> — show
page() - Images —
content://sources - $permission — what a folder grant replaces