remove old icon picker
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import {openDB, deleteDB} from "idb"
|
||||
import type {IDBPDatabase} from "idb"
|
||||
import {writable} from "svelte/store"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {call, defer} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {withGetter} from "@welshman/store"
|
||||
|
||||
export type IDBAdapter = {
|
||||
name: string
|
||||
keyPath: string[]
|
||||
init: (table: IDBTable<any>) => Promise<Unsubscriber>
|
||||
}
|
||||
|
||||
export type IDBAdapters = IDBAdapter[]
|
||||
|
||||
export enum IDBStatus {
|
||||
Ready = "ready",
|
||||
Closed = "closed",
|
||||
Opening = "opening",
|
||||
Closing = "closing",
|
||||
Initial = "initial",
|
||||
}
|
||||
|
||||
export type IDBOptions = {
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export class IDB {
|
||||
idbp: Maybe<Promise<IDBPDatabase>>
|
||||
ready: Maybe<Promise<void>>
|
||||
unsubscribers: Maybe<Unsubscriber[]>
|
||||
status = IDBStatus.Initial
|
||||
|
||||
constructor(readonly options: IDBOptions) {}
|
||||
|
||||
init(adapters: IDBAdapters) {
|
||||
if (this.status !== IDBStatus.Initial) {
|
||||
throw new Error(`Database re-initialized while ${this.status}`)
|
||||
}
|
||||
|
||||
this.status = IDBStatus.Opening
|
||||
|
||||
this.idbp = openDB(this.options.name, this.options.version, {
|
||||
upgrade(idbDb: IDBPDatabase) {
|
||||
const names = new Set(adapters.map(a => a.name))
|
||||
|
||||
for (const table of idbDb.objectStoreNames) {
|
||||
if (!names.has(table)) {
|
||||
idbDb.deleteObjectStore(table)
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, keyPath} of adapters) {
|
||||
try {
|
||||
idbDb.createObjectStore(name, {keyPath})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
blocked() {},
|
||||
blocking() {},
|
||||
})
|
||||
|
||||
this.ready = this.idbp.then(async idbp => {
|
||||
window.addEventListener("beforeunload", () => idbp.close())
|
||||
|
||||
this.unsubscribers = await Promise.all(adapters.map(({name, init}) => init(this.table(name))))
|
||||
|
||||
this.status = IDBStatus.Ready
|
||||
})
|
||||
}
|
||||
|
||||
table = <T>(name: string) => new IDBTable<T>(this, name)
|
||||
|
||||
_withIDBP = async <T>(f: (db: IDBPDatabase) => Promise<T>) => {
|
||||
if (this.status === IDBStatus.Initial) {
|
||||
throw new Error("Database was accessed in initial state")
|
||||
}
|
||||
|
||||
// If we're closing, ignore any lingering requests
|
||||
if ([IDBStatus.Closed, IDBStatus.Closing].includes(this.status)) return
|
||||
|
||||
return f(await this.idbp)
|
||||
}
|
||||
|
||||
getAll = async <T>(table: string): Promise<T[]> =>
|
||||
this._withIDBP(async idbp => {
|
||||
const tx = idbp.transaction(table, "readwrite")
|
||||
const store = tx.objectStore(table)
|
||||
const result = await store.getAll()
|
||||
|
||||
await tx.done
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
bulkPut = async <T>(table: string, data: Iterable<T>) =>
|
||||
this._withIDBP(async idbp => {
|
||||
const tx = idbp.transaction(table, "readwrite")
|
||||
const store = tx.objectStore(table)
|
||||
|
||||
await Promise.all(
|
||||
Array.from(data).map(item => {
|
||||
try {
|
||||
store.put(item)
|
||||
} catch (e) {
|
||||
console.error(e, item)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await tx.done
|
||||
})
|
||||
|
||||
bulkDelete = async (table: string, ids: Iterable<string>) =>
|
||||
this._withIDBP(async idbp => {
|
||||
const tx = idbp.transaction(table, "readwrite")
|
||||
const store = tx.objectStore(table)
|
||||
|
||||
await Promise.all(Array.from(ids).map(id => store.delete(id)))
|
||||
await tx.done
|
||||
})
|
||||
|
||||
close = () =>
|
||||
this._withIDBP(async idbp => {
|
||||
this.unsubscribers!.forEach(call)
|
||||
this.status = IDBStatus.Closing
|
||||
|
||||
await idbp.close()
|
||||
|
||||
// Allow the caller to call reset and re-init immediately
|
||||
if (this.status === IDBStatus.Closing) {
|
||||
this.idbp = undefined
|
||||
this.ready = undefined
|
||||
this.unsubscribers = undefined
|
||||
this.status = IDBStatus.Closed
|
||||
}
|
||||
})
|
||||
|
||||
clear = async () => {
|
||||
await this.close()
|
||||
await deleteDB(this.options.name, {
|
||||
blocked() {},
|
||||
})
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
if (![IDBStatus.Closing, IDBStatus.Closed].includes(this.status)) {
|
||||
throw new Error("Database reset when not closed")
|
||||
}
|
||||
|
||||
this.status = IDBStatus.Initial
|
||||
}
|
||||
}
|
||||
|
||||
export class IDBTable<T> {
|
||||
constructor(
|
||||
readonly db: IDB,
|
||||
readonly name: string,
|
||||
) {}
|
||||
|
||||
getAll = () => this.db.getAll<T>(this.name)
|
||||
|
||||
bulkPut = (data: Iterable<T>) => this.db.bulkPut(this.name, data)
|
||||
|
||||
bulkDelete = (ids: Iterable<string>) => this.db.bulkDelete(this.name, ids)
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import {reject, identity} from "@welshman/lib"
|
||||
import {type StorageProvider} from "@welshman/store"
|
||||
import {Preferences} from "@capacitor/preferences"
|
||||
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
|
||||
|
||||
export class PreferencesStorageProvider implements StorageProvider {
|
||||
p = Promise.resolve()
|
||||
|
||||
get = async <T>(key: string): Promise<T | undefined> => {
|
||||
const result = await Preferences.get({key})
|
||||
if (!result.value) return undefined
|
||||
try {
|
||||
return JSON.parse(result.value)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
set = async <T>(key: string, value: T): Promise<void> => {
|
||||
this.p = this.p.then(() => Preferences.set({key, value: JSON.stringify(value)}))
|
||||
|
||||
await this.p
|
||||
}
|
||||
|
||||
clear = async () => {
|
||||
this.p = this.p.then(() => Preferences.clear())
|
||||
|
||||
await this.p
|
||||
}
|
||||
}
|
||||
|
||||
export const preferencesStorageProvider = new PreferencesStorageProvider()
|
||||
|
||||
export type CollectionOptions<T> = {
|
||||
table: string
|
||||
getId: (item: T) => string
|
||||
}
|
||||
|
||||
export class Collection<T> {
|
||||
constructor(readonly options: CollectionOptions<T>) {}
|
||||
|
||||
static clearAll = async (): Promise<void> => {
|
||||
const res = await Filesystem.readdir({
|
||||
path: "",
|
||||
directory: Directory.Data,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
res.files.map(file =>
|
||||
Filesystem.deleteFile({
|
||||
path: file.name,
|
||||
directory: Directory.Data,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#path = () => `collection_${this.options.table}.json`
|
||||
|
||||
get = async (): Promise<T[]> => {
|
||||
try {
|
||||
const file = await Filesystem.readFile({
|
||||
path: this.#path(),
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
})
|
||||
|
||||
// Speed things up by parsing only once
|
||||
return JSON.parse("[" + file.data.toString().split("\n").filter(identity).join(",") + "]")
|
||||
} catch (err) {
|
||||
// file doesn't exist, or isn't valid json
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
set = (items: T[]) =>
|
||||
Filesystem.writeFile({
|
||||
path: this.#path(),
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
data: items.map(v => JSON.stringify(v)).join("\n"),
|
||||
})
|
||||
|
||||
add = (items: T[]) =>
|
||||
Filesystem.appendFile({
|
||||
path: this.#path(),
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
data: "\n" + items.map(v => JSON.stringify(v)).join("\n"),
|
||||
})
|
||||
|
||||
remove = async (ids: Set<string>) =>
|
||||
this.set(reject(item => ids.has(this.options.getId(item)), await this.get()))
|
||||
|
||||
update = async ({add, remove}: {add?: T[]; remove?: Set<string>}) => {
|
||||
if (remove && remove.size > 0) {
|
||||
const items = reject(item => remove.has(this.options.getId(item)), await this.get())
|
||||
|
||||
if (add) {
|
||||
items.push(...add)
|
||||
}
|
||||
|
||||
await this.set(items)
|
||||
} else if (add && add.length > 0) {
|
||||
await this.add(add)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user