Aller au contenu

Utilisateur:Yopyop456/Brouillon/utils

Une page de Wikipédia, l'encyclopédie libre.
export const version = 'v2024-0429'

/*
fetch from cache
url: string
options: request options
options.cacheReload: boolean
return: response
*/
export async function cacheFetch(url, options = {}){
  if(options.cacheReload) await cacheClear(url)
  return cachePut(url, undefined, options)
}

/*
put in cache
url: string
content: string or buf
options: response options
options.cacheName: string
return: response
*/
export async function cachePut(url, content, options = {}){
  let request, response, cache
  cache = await caches.open(options.cacheName || "v1")

  request = new Request(url, content ? {} : options)
  response = await cache.match(request)

  if(typeof content != 'undefined'){
    if(response) return console.warn('cachePut error, already in cache') || true
    request = new Request(url)
    response = new Response(content, options)
    await cache.put(request, response)
    return true
  }

  if(!response){
    response = await fetch(request)
    if(response.status == 200) {
      await cache.put(request, response.clone());
    }
  }

  return response
}

/*
clear cache
re: regex string filter url
options.cacheName: string
*/
export async function cacheClear(re, options = {}){
  if(!re){
    await caches.delete(options.cacheName || 'v1')
    return []
  }
  
  options.cacheDelete = true
  return await cacheGetOutdated(re, options)
}

/*
get outdated cached files
substr: sub string filter url
options.cacheName: string
options.cacheDelete: boolean
*/

export async function cacheGetOutdated(substr, options = {}){
  let cache, keys, key, res, web, local
  cache = await caches.open(options.cacheName || 'v1')
  keys = await cache.keys()
  res = []

  for(key of keys){
    if(key.url.includes(substr)) {

      if(options.cacheDelete){
        res.push(key.url)
        await cache.delete(key)
        continue
      }

      web = await fetchHead(key.url)
      local = await cache.match(key).then(x=>x.arrayBuffer())
      if(!web['content-length']){
        console.warn('no content-length: ' +key.url)
      }
      else if(web['content-length'] != local.byteLength){
        res.push(key.url)
      }
    }
  }

  return res
}

/*
merge buffers
tarray: Array of TypedArray
keepOriginal: bool
return: TypedArray

let i, a, b
console.time("aaa")
for(i=1; i<110000; i++){
  a = new Uint8Array(i).fill(1)
  b = new Uint8Array(i).fill(2)
  _.mergeBuf([a, b], true)
}
console.timeEnd("aaa")

console.time("bbb")
for(i=1; i<110000; i++){
  a = new Uint8Array(i).fill(1)
  b = new Uint8Array(i).fill(2)
  _.mergeBuf([a, b], false)
}
console.timeEnd("bbb")

*/
export function mergeBuf(tarray, keepOriginal = true){
  let size, buf, view, i
  size = 0

  for(i in tarray) size += tarray[i].byteLength

  if(tarray[0].buffer.transfer){
    i = keepOriginal ? 0 : tarray[0].byteLength
    view = new tarray[0].constructor(keepOriginal ? size : tarray[0].buffer.transfer(size))
    size = i
    if(tarray[0].buffer.detached) tarray[0] = new tarray[0].constructor()
  }
  else {
    buf = new ArrayBuffer(keepOriginal ? size : 0, {maxByteLength: size})
    view = new tarray[0].constructor(buf)
    size = 0
  }

  for(i in tarray){
    if(buf && !keepOriginal) buf.resize(size + tarray[i].byteLength)
    view.set(tarray[i], size / tarray[i].BYTES_PER_ELEMENT)
    size += tarray[i].byteLength
    if(!keepOriginal) tarray[i] = null
  }

  if(buf){
    buf = buf.transferToFixedLength()
    view = new view.constructor(buf)
  }

  return view
}

/*
fetch with download progress
url: string
options: fetch options
options.callback : function or selector string
options.resume: bool resume from partial content cached
fetchProgress.abort(resumable) : abort fetch / resumable bool
return: TypedArray
*/
export async function fetchProgress(url, options = {}){
  if(fetchProgress.abort){
    throw new Error('ongoing fetch')
  }

  let controller = new AbortController();
  options.signal = controller.signal

  let response, reader, contentLength, result
  let receivedLength = 0; 
  let chunks = [];

  if(options.resume){
    response = await cacheFetch(url).then(x=>x.arrayBuffer())
    await cacheClear(url)
    chunks.push(response)

    if(!options.headers) options.headers = {}
    options.headers.range = 'bytes=' + response.byteLength + '-'
  }

  response = await fetch(url, options);
  reader = response.body.getReader();
  contentLength = +response.headers.get('Content-Length');
  if(!contentLength) console.warn('not a range request')

  fetchProgress.abort = async (resumable = false) => {
    controller.abort()
    delete fetchProgress.abort
    if(!(resumable && contentLength)) return

    result = mergeBuf(chunks, false)
    await cachePut(url, result, {status: 202})
  }

  while(true) {
    const {done, value} = await reader.read();
    if (done) break;
    chunks.push(value);
    receivedLength += value.length;

    if(typeof options.callback == 'function'){
      options.callback(receivedLength, contentLength)
    }
    else if(typeof options.callback == 'string'){
      let div = document.querySelector(options.callback)
      let prog = div.querySelector('progress')
      if(!prog){
        prog = document.createElement('progress')
        div.appendChild(prog)
        prog.max = contentLength
      }
      else prog.max = contentLength
      prog.value = receivedLength
    }
  }

  result = mergeBuf(chunks, false)
  delete fetchProgress.abort

  return result
}

/*
wait for a specific amount of time
ms : int
*/
export function sleep(ms){
  return new Promise(r => setTimeout(r, ms | 0))
}

/*
load a script
s: script text or js / mjs url
div: html element or div id to append, default document.head
return: html element
*/
export function loadScript(s, div = '') {
  return new Promise(r => {
    let elm = document.createElement('script')
    elm.onload = () => r(elm)
    elm.id = 'script' + document.querySelectorAll('script').length
    if(s.includes('.mjs')) {
      // elm.id = /(\w+).mjs/.exec(s)?.at(1)
      elm.type = 'module'
      elm.innerHTML = `
(async ()=>{
window.${elm.id} = await import('${s}');
document.getElementById('${elm.id}').dispatchEvent(new Event('load'));
})()
`
    }
    else if(s.includes('.js')) elm.src = s
    else elm.innerHTML = s + `\ndocument.getElementById('${elm.id}').dispatchEvent(new Event('load'));`
    div = div?.constructor?.name == 'HTMLBodyElement' ? div : (document.getElementById(div) || document.head)
    div.appendChild(elm)
  })
}

/*
load a stylesheet
s: style text or css url
div: html element or div id to append, default document.head
return: html element
*/
export function loadStyle(s, div = '') {
  return new Promise(r => {
    let elm
    if(s.includes('.css')) {
      elm = document.createElement('link')
      elm.rel = 'stylesheet'
      elm.href = s
    }
    else {
      elm = document.createElement('style')
      elm.innerHTML = s
    }
    elm.onload = () => r(elm)
    div = div?.constructor?.name == 'HTMLBodyElement' ? div : (document.getElementById(div) || document.head)
    div.appendChild(elm)
  })
}

/*
set dark mode css
*/
export async function setDarkMode(){
  let csstext
  csstext = `
@media (prefers-color-scheme: dark) {
  html {
    filter: invert(100%);
  }
}
`
  if(getComputedStyle(document.body).background.includes('0, 0, 0')){
    csstext += 'body { background: white }'
  }

  return await loadStyle(csstext)
}

/*
fetch multiple url simultaneously
cache: object {key url : value html}
options.max: max number of simultaneous connections
options.urlmodif: fetch url modifier
options.valmodif: fetch result modifier
options.method: string
options.responsetype: string

let cache = {
"https://huggingface.co/Xenova/Qwen1.5-0.5B-Chat/raw/main/merges.txt":"",
"https://huggingface.co/Xenova/Qwen1.5-0.5B-Chat/raw/main/tokenizer.json":"",
"https://huggingface.co/Xenova/Qwen1.5-0.5B-Chat/raw/main/vocab.json":""
}

*/
export function fetchAll (cache, options = {}) {
  let {max = 4, urlmodif, valmodif, method, responsetype = "text"} = options

  return typeof cache == 'object' && new Promise(r => {
    let a, links, count, nth

    if(fetchAll.abort) throw new Error('ongoing fetch')
    fetchAll.abort = ()=>{max = 0}

    links = []
    for(a in cache) if(a.includes('http') && cache[a] == '') links.push(a)
    count = 0
    nth = 0

    for(a=0; a<max; a++) getNext()

    async function fetchOne(nth){
      cache[links[nth]] = '...'
      let txt, url, args
      url = typeof urlmodif == 'function' ? (await urlmodif(links[nth])) : links[nth];
      if(method == 'post'){
        txt = url.split('?')
        url = txt[0]
        args = {
          "headers": {
            "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
          },
          "body": txt[1] || '',
          "method": "POST"
        }
      } else {
        args = {}
      }

      txt = await fetch(url, args).then(x=>x[responsetype]())
      cache[links[nth]] = typeof valmodif == 'function' ?  (await valmodif(txt)) : txt
      count--

      getNext()
    }

    function getNext(){
      if(nth >= links.length || !max){
        if(count) return
        else {
          delete fetchAll.abort
          return r('finish')
        }
      }
      if(count >= max) return;
      count++
      fetchOne(nth++)
    }

  })
}

/*
Limit functions call. Execute once no call during a pediod of time
f : function to execute
ms : time to wait
return : new function
*/
export function delayfct(fn, ms=100){
  let running = false

  return () => {
    function isrunning() {
      if(running) return;
      running = true;
      setTimeout(function() {
        running = false;
        fn()
      }, ms);
    }

    isrunning()
  }
}

/*
Donwload a file
file : blob or string
name : filename, default creation date time
*/

export function download(file, name="") {
  if(!(file instanceof Blob)) file = new Blob([file]);
  let url = URL.createObjectURL(file);
  let a = document.createElement("a");
  a.href = url;
  if(!name || name[0] == '.') {
    let dat = (new Date()).toISOString()
    name = dat.slice(0, 10).replaceAll("-", "_") + '_' + dat.slice(-7).replaceAll(/\D/g,"") + name
  }
  a.download = name;
  a.style.display = "none";
  document.body.appendChild(a);
  if(name.trim()){
    a.click();
    setTimeout(()=>a.remove(), 100)
  } else return a
}

/*
Fetch file head
url: string
*/
export async function fetchHead(url){
  return await fetch(url, {method: 'HEAD'}).then((result) => {
    let head = {}
    for(let key of result.headers.keys()){
      head[key] = result.headers.get(key)
    }
    return head
    // return result.headers.get("content-length")
  })
}

/*
# 1 - ArrayBuffer utilities
*/

/*
string to buffer
str: string, number (16 x 16 hex values)
return: uint8
*/
export function str2buf(str, nextarr = null){
  let encoder, res, tmp

  if(typeof str == "string") {
    encoder = new TextEncoder("utf-8");
    res = encoder.encode(str);
  }
  else if(typeof str == "number") {
    res = new Uint8Array(16);
    for(tmp=15; tmp>=0; tmp--){
      res[tmp] = str%16
      str = (str - (str%16)) / 16
    }
  }
  else {
    res = new Uint8Array(str);
  }

  if(nextarr){
    nextarr = str2buf(nextarr)
    res = mergeBuf([str, nextarr])
  }

  return res
}

/*
buffer to string
*/
export function buf2str(buffer) {
  let decoder = new TextDecoder("utf-8");
  return decoder.decode(buffer);
}

/*
buffer to base64
b64: string
url: url friendly escape chars
*/
export function buf2b64(buffer, url=false) {
  // btoa(String.fromCharCode.apply(null, uint8));
  let binary = '';
  let bytes = new Uint8Array( buffer );
  let len = bytes.byteLength;
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode( bytes[ i ] );
  }
  let b64 = btoa( binary );
  return url ? b64.replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') : b64;
}


/*
base64 to buffer
b64: string
url: url friendly escape chars
*/
export function b642buf(b64, url=false){
  // Uint8Array.from(atob( str ), c => c.charCodeAt(0))
  if(url) b64 = b64.replaceAll(/_/g, '/').replaceAll(/-/g, '+');

  let binary_string = atob(b64);
  let len = binary_string.length;
  let bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i);
  }
  return bytes;
}

/*
buf to hex
buf: string
alt: bool alternative representation
*/
export function buf2hex(buf, alt=false) {
  let bytes = new Uint8Array(buf);
  let hex = "";
  for(let i=0; i<bytes.length; i++){
    hex += alt ? "\\x" : "";
    hex += ('0' + bytes[i].toString(16)).slice(-2);
  }
  return hex;
}

/*
hex to buf
hexString: string
*/
export function hex2buf(hexString) {
  let result = [];
  hexString = hexString.replaceAll('\\x', '')
  for(let i=0; i < hexString.length; i+=2) {
     result.push(parseInt(hexString.substr(i, 2), 16));
  }
  return (new Uint8Array(result));
}


/*
# 2 - Miscellaneous
*/

/*
Read file input
id : element id string, HTML Element, Array of Files
fn : function modifier
changeInputElm : boolean update files list in input
*/
export function readfiles (id, fn=null, changeInputElm=false){
  return new Promise(r=>{
    let input, files, len, result, file, a

    if(id?.constructor?.name == 'Array' || id?.constructor?.name == 'FileList') input = {files: id}
    else if(id?.constructor?.name == 'HTMLBodyElement') input = id
    else input = document.getElementById(id)

    files = input.files
    len = files.length
    const reader = new FileReader();
    const dataTransfer = new DataTransfer()
    result = {}
    readfile(0)

    function readfile(x){
      reader.onload = (event) => {
        result[files[x].name] = new Uint8Array(event.target.result)

        if(x+1 == len) process()
        else readfile(x+1)
      }
      reader.readAsArrayBuffer(files[x]);
    }

    async function process(){
      if(typeof fn == 'function'){
        await fn(result)
      }
      if(changeInputElm){
        for(a in result){
          if(a.length == 1) continue
          file = new File([result[a]], a)
          dataTransfer.items.add(file)
        }
        input.files = dataTransfer.files
      }
      r(result)
    }

  });

};

/*
save user setting, using method. If no val, get the data
val: string value
method: string cookie or localStorage
*/
export function saveUserSettings(val, method='cookie'){
  if(method == 'cookie'){
    if(val){
      let s = val.includes('=') ? 86400 * 14 : -1;
      return document.cookie =  val+';max-age='+s+';sameSite;secure'
    }

    let elms, i, res
    res = {}
    elms = document.cookie.replaceAll('; ', '=')
    elms = elms ? elms.split('=') : []
    for(i=0; i<elms.length; i+=2) res[elms[i]] = elms[i+1]

    return res
  }
  else if(method == 'localStorage'){
    let key = location.pathname.substring(1).replaceAll('/', '_')
    if(val) localStorage.setItem(key, typeof val == 'string' ? val : JSON.stringify(val))
    else return JSON.parse(localStorage.getItem(key))
  }
}

/*
post data to url
url : url
obj : formdata, selector string, or key value object
return fetch
*/
export function post(url, obj){
  let data
  if(obj?.constructor?.name == 'FormData') data = obj
  else if(typeof obj == 'string') data = new FormData(document.querySelector(obj))
  else {
    data = new FormData();
    for(let key in obj){
      data.append(key, obj[key])
    }
  }

  if(!url) return new Promise((r)=>r(data))

  return fetch(url, {
    method: "post",
    body: data
  })
}

/*
https://github.com/stephenjjbrown/string-similarity-js
string similarity based on the Sørensen–Dice coefficient
str1: string
str2: string
substringLength: string
caseSensitive: false
*/
export function stringSimilarity(str1, str2, substringLength = 2, caseSensitive = false) {
  if (!caseSensitive) {
    str1 = str1.toLowerCase();
    str2 = str2.toLowerCase();
  }
  if (str1.length < substringLength || str2.length < substringLength)
    return 0;
  var map = new Map();
  for (var i = 0; i < str1.length - (substringLength - 1); i++) {
    var substr1 = str1.substr(i, substringLength);
    map.set(substr1, map.has(substr1) ? map.get(substr1) + 1 : 1);
  }
  var match = 0;
  for (var j = 0; j < str2.length - (substringLength - 1); j++) {
    var substr2 = str2.substr(j, substringLength);
    var count = map.has(substr2) ? map.get(substr2) : 0;
    if (count > 0) {
      map.set(substr2, count - 1);
      match++;
    }
  }
  return (match * 2) / (str1.length + str2.length - ((substringLength - 1) * 2));
};

/*
Escape html entities
txt: string
*/
export function escapeHTML(txt){
  let elm = document.createElement('textarea');
  elm.textContent = txt
  txt = elm.innerHTML
  return txt
}

/*
Compress file using gzip deflate or deflate-raw
blob: blob or Uint8Array
dec: boolean decompress instead
*/
export async function compress(blob, dec = false){
  if(blob?.constructor?.name == 'Uint8Array') blob = new Blob([blob])

  const stream = dec ? new DecompressionStream("gzip") : new CompressionStream("gzip");
  
  const readablestream = blob.stream().pipeThrough(stream);
  return await new Response(readablestream).blob();
}

/*
Decompress file using gzip deflate or deflate-raw
blob: blob or Uint8Array
*/
export async function decompress(blob){
  return await compress(blob, true)
}


/**
TAR HEADER

Field offset	Field size	Field
0	100	File name
100	8	File mode (octal)
108	8	Owner's numeric user ID (octal)
116	8	Group's numeric user ID (octal)
124	12	File size in bytes (octal, null terminated)
136	12	Last modification time in numeric Unix time format (octal, null terminated)
148	8	Checksum for header record
156	1	Link indicator (file type: 0 file, 5 folder)
157	100	Name of linked file

format ustar
257 "ustar"
262 version "00"
345 préfixe fichier

chunks of 512 bytes
blockchunks of 10240 bytes
end with two empty chunks
**/

/*
create tar archive
arr: n+0 filename n+1 arraybuf || key value object
opt.folder string
return: {tar buffer, idx n+0 position n+1 length}
*/
export function tarCreate(arr, opt = {}){
  let i, j, bufsize = 0, buf, bloc, offset, idx, tmp;
  if(typeof arr != "object") throw new Error('arr: not an object')
  if(arr instanceof Array){
    if(arr.length == 0) return;
    if(arr.length%2) return;
  } else {
    tmp = []
    for(i in arr){
      if(!(typeof arr[i] == 'object' && arr[i].constructor.name.includes('Array'))) continue
      tmp.push(i.replace(/.+\//, ''), arr[i])
    }
    arr = tmp
  }

  idx = [];

  bufsize += 512;
  for(i=0; i<arr.length; i+=2){
    arr[i+1] = new Uint8Array(arr[i+1])
    j = arr[i+1].byteLength;
    j = 1024 + j - ((j-1)%512) - 1;
    bufsize += j
  }
  bufsize += 1024;

  bufsize = 10240 + bufsize - ((bufsize-1)%10240) - 1;
  buf = new ArrayBuffer(bufsize);

  offset = 0;
  for(i=0; i<arr.length; i+=2){
    bloc = new Uint8Array(buf, offset, 512);
    bloc.set(str2buf("" + arr[i].substring(0, 100)), 0);
    bloc.set(str2buf("0000775"), 100);
    bloc.set(str2buf("0001750"), 108);
    bloc.set(str2buf("0001750"), 116);
    j = ("00000000000"+(arr[i+1].byteLength).toString(8)).slice(-11);
    bloc.set(str2buf(j), 124);
    j = ("00000000000"+(Math.trunc(Date.now()/1000)).toString(8)).slice(-11);
    bloc.set(str2buf(j), 136);
    j = arr[i+1].byteLength == 0 ? "5" : "0";
    // 148
    bloc.set(str2buf(j), 156);
    bloc.set(str2buf("ustar"), 257);
    bloc.set(str2buf("00"), 262);
    bloc.set(str2buf('folder' in opt ? opt.folder : '_'), 345);

    let chksum = 32 * 8;
    for(j = 0; j < 512; j++) {
      chksum += bloc[j];
    }
    bloc.set(str2buf(chksum.toString(8)), 148);

    offset += 512;

    j = arr[i+1].byteLength;
    if(!j) continue;
    idx.push(offset, j);

    j = 512 + j - ((j-1)%512)-1;
    bloc = new Uint8Array(buf, offset, j);
    bloc.set(arr[i+1], 0);
    offset += j;
    // debugger;
  }

  return {tar:buf, idx:idx};
}

/*
append tar
first: object from tarCreate
second: object from tarCreate
*/
export function tarAppend(first, second, opt = {}){
  let a, firstLen, secondLen, firstIdx, secondIdx, bufsize, result
  
  if(!first && second) return tarCreate(second, opt)

  first = first.tar ? first : tarCreate(fist, opt)
  firstIdx = first.idx
  first = new Uint8Array(first.tar)
  for(a = first.byteLength - 1024; a>=0 ;a--){
    if(first[a]) break
  }
  firstLen = 512 + a - ((a-1)%512) - 1;

  second = second.tar ? second : tarCreate(second, opt)
  secondIdx = second.idx
  second = new Uint8Array(second.tar)
  for(a = second.byteLength - 1024; a>=0 ;a--){
    if(second[a]) break
  }
  secondLen = 512 + a - ((a-1)%512) - 1;

  bufsize = firstLen + secondLen + 1024
  bufsize = 10240 + bufsize - ((bufsize-1)%10240) - 1;

  result = new Uint8Array(bufsize)
  result.set(first.subarray(0, firstLen))
  result.set(second.subarray(0, secondLen), firstLen)

  for(a=0; a<secondIdx.length; a+=2){
    secondIdx[a] = secondIdx[a] + firstLen
  }
  firstIdx.push(...secondIdx)

  return {tar: result.buffer, idx: firstIdx}
}

/*
create webworker
str: js filepath or string with a main function
*/
export function workerCreate(str = ''){
  let worker
  if(str.includes('.js')){
    worker = new Worker(str)
  }
  else if(str.includes('main')) {
    str += `
self.addEventListener('message', function(e) {
  self.postMessage(main(e.data))
});
`
    str = new Blob([str2buf(str)])
    str = URL.createObjectURL(str)
    worker = new Worker(str)
  }
  else throw new Error('js filepath or string with a main function')

  return worker
}

/*
run unit test
tests: {testName: [testFn, expected value]}
timeLog: boolean
*/
export async function assert(tests = {}, timeLog = false){
  
  let line, out, testName, testRes, testFn, expectVal, AsyncFunction
  AsyncFunction = Object.getPrototypeOf(async ()=>{}).constructor
  out = ''
  
  for(testName in tests){

    if(typeof tests[testName] != 'object'){
      tests[testName] = [tests[testName]]
    }

    if(typeof tests[testName][0] == 'string'){
      testFn = (tests[testName][0].includes('return') ? '' : 'return await ') + tests[testName][0]
      testFn = new AsyncFunction(testFn)
    }
    else if(typeof tests[testName][0] == 'function'){
      testFn = tests[testName][0]
    }
    else continue

    try {
      if(timeLog) console.time(testName)
      testRes = await testFn()
      if(timeLog) console.timeEnd(testName)

      if(typeof tests[testName][1] != 'undefined') expectVal = tests[testName][1]
      else {
        testRes = testRes ? true : false
        expectVal = true
      }
      if(testRes.constructor.name == 'Object'){
        testRes = JSON.stringify(testRes)
        expectVal = JSON.stringify(expectVal)
      }
      else{
        testRes = testRes.toString()
        expectVal = expectVal.toString()
      }
      if(testRes != expectVal){
        throw new Error(testRes + ' != ' + expectVal)
      }
      
      line = testName + ': OK' + '\n'
    }
    catch(e){
      line = testName + ': Failed (' + e.message + ')\n'
    }
    console.log(line)
    out += line
  }
  
  return out
	
}

/*
change url without reload
newURL: string
push: boolean
*/
export function changeURL(newURL, push = false){
  if(push) history.pushState({}, null, newURL)
  else history.replaceState({}, null, newURL);
}

/*
read (if no text) or write clip board
text: string
*/
export async function clipboard(text){
  console.log(text)
  if(text) await navigator.clipboard.writeText(text)
  else await navigator.clipboard.readText()
}

/*
access to file system
handle: expected string(openFile, saveFile, directory, OPFS, clearOPFS) or object [handle]
data: data to write ('-' to delete)
path: file path if directory
*/
export async function fileSys(handle="OPFS", data, path){

  async function getSubFile(dirHandle, path, create = false){
    let retHandle
    path = path.split('/')
    retHandle = dirHandle
    for(let i=0; ; i++){
      if(i < path.length - 1){
        retHandle = await retHandle.getDirectoryHandle(path[i], { create })
      }
      else {
        retHandle = await retHandle.getFileHandle(path[i], { create })
        break
      }
    }
    return retHandle
  }

  let ret, handle2

  if(handle == "openFile"){
    [handle] = await showOpenFilePicker({multiple: false})
    ret = await handle.getFile()
  }
  else if(handle == "saveFile"){
    handle = await showSaveFilePicker()
  }
  else if(handle == "directory"){
    handle = await showDirectoryPicker()
  }
  else if(handle == "OPFS"){
    handle = await navigator.storage.getDirectory()
  }
  else if(handle == "clearOPFS"){
    await navigator.storage.getDirectory().then(x=>x.remove({ recursive: true }))
  }
  else if(typeof handle[0] == "object"){
    handle = handle[0]
  }
  else throw new Error('handle: expected string(openFile, saveFile, directory, OPFS, clearOPFS) or object [handle]')

  if(typeof data != 'undefined' && data){
    if(handle.constructor.name == 'FileSystemDirectoryHandle'){
      if(typeof path == 'undefined') throw new Error('no path for DirectoryHandle')
      handle2 = await getSubFile(handle, path, true)
      if(data == "-") {
        await handle2.remove()
        return [handle, ret]
      }
    }
    else handle2 = handle
    handle2 = await handle2.createWritable()
    await handle2.write(data)
    await handle2.close()
  }
  else if(path && handle.constructor.name == 'FileSystemDirectoryHandle'){
    ret = await getSubFile(handle, path).then(x=>x.getFile())
  }
  else if(handle.constructor.name == 'FileSystemDirectoryHandle'){
    ret = []
    for await (let [name, fileHandle] of handle) {
      if(fileHandle.constructor.name.includes('Directory')){
        let list, a
        list = (await fileSys([fileHandle]))[1]
        for(a in list) list[a] = name + '/' + list[a]

        ret.push(name + '/', ...list)
      }
      else ret.push(name)
    }
  }

  return [handle, ret]
}

/*
fullscreen
element: string element id, or html element
*/
export function fullscreen(elm){
  if(typeof id == 'string') document.getElementById(elm).requestFullscreen() 
  else elm.requestFullscreen() 
}

/*
get geo location
*/
export function geolocation(){
  return new Promise(r=>{
    navigator.geolocation.getCurrentPosition((x)=>r(x))
  })
}

/*
byteArray
*/
export function byteArray(n){
  class ByteArray{
    constructor(n){
      this.arr = ArrayBuffer.isView(n) ? n : new Uint8Array(1+((n/8)|0))
    }
    get(n){
      return this.arr[((n/8)|0)] & (1<<(n%8))
    }
    set(n){
      this.arr[((n/8)|0)] |= (1<<(n%8))
    }
    unset(n){
      this.arr[((n/8)|0)] &= ~(1<<(n%8))
    }
  }
  return new ByteArray(n)
}

/*
get object from string object name
*/
export function objectFromString(str, lexicallyScoped = false){
  function makeFunction() {
    var params = [];
    for (var i = 0; i < arguments.length -  1; i++) {
      params.push(arguments[i]);
    }
    var code = arguments[arguments.length -  1];
  
   return eval('[function (' + params.join(',')+ '){' + code + '}][0]');
  }

  let fn
  str = 'return typeof '+str+' == "undefined" || '+str
  fn = lexicallyScoped ? makeFunction(str) : new Function(str)
  return fn
}

/*
WIP
*/
export async function webCrawl(url, depth = 5){
  let a, b, page, webPages, relPath, basePath, m
  webPages = {[url] : ''}
  relPath = /^.+\//.exec(url)?.at(0)
  basePath = /^[^\.]+[^\/]+/.exec(url)?.at(0)

  for(a=0; a<depth; a++){
    await fetchAll(webPages)
    for(b in webPages){
      page = webPages[b]
      if(!b.match(/css$|js$/) && page.length > 5){
        [
          /<link[^>]+href ?= ?['"]([^'"]+)['"]/g,
          /<script[^>]+src ?= ?['"]([^'"]+)['"]/g,
          /<a[^>]+href ?= ?['"]([^#][^'"]+)['"]/g,
        ].forEach(re=>{
          while(m = re.exec(page)){
            m = m.at(1)
            m = m.includes('http') ? m : m[0] == '/' ? basePath + m : relPath + m
            if(!webPages[m]) webPages[m] = ''
          }
        })
      }
      webPages[b] = 'xxx'
    }
  }

  return webPages
}

/*
SIMPLE HACK
*/
export function wrapFetch(callback){
  if(!fetch.toString().includes('native')) return

  let _fetch = fetch
  fetch = (...args) => {
    callback(args)
    return _fetch.call(window, ...args)
  }
}

export function wrapEventListener(){
  Node.prototype._addEventListener = Node.prototype.addEventListener;
  Node.prototype._removeEventListener = Node.prototype.removeEventListener;

  Node.prototype.addEventListener = function(a, b, c){
    if(!this._eventListeners) this._eventListeners = new Array()
    this._eventListeners.push({a, b, c})
    this._addEventListener(a, b, c)
  };

  Node.prototype.removeEventListener = function(a, b, c){
    let i
    if(!this._eventListeners) return
    for(i = this._eventListeners.length-1; i>=0; i--) if(b==this._eventListeners[i].b) break
    if(i<0) return
    this._eventListeners.splice(i, 1)
    this._removeEventListener(a, b, c)
  }
}

export function wrapResizeObs(callback, elmId){
  if(!callback) callback = (entries) => {
    entries.forEach(entry => {
      console.log(entry.target, entry.contentRect)
    })
  }
  const observer = new ResizeObserver(callback)
  if(elmId) observer.observe(document.getElementById(elmId), { box: 'border-box' })

  return observer
}

export function wrapIntersectionObs(callback, elmId){
  if(!callback) callback = (entries) => {
    entries.forEach(entry => {
      console.log(entry.target, entry.isIntersecting)
    })
  }    
  const observer = new IntersectionObserver(callback, { threshold: 0 })
  if(elmId) observer.observe(document.getElementById(elmId))

  return observer
}

export function wrapMutationObs(callback, elmId){
  if(!callback) callback = (mutationList) => {
		for(let mutation of mutationList){
			for(let child of mutation.addedNodes){
				console.log(child)
			}
		}
  }
  const observer = new MutationObserver(callback)
  if(elmId) observer.observe(document.getElementById(elmId), {childList: true, subtree: false})

  return observer
}

export function wrapEvent(name, elmId){
  let ev = {}
  ev.tabev = new KeyboardEvent('keydown', {'key': 'Tab', "keyCode":9, bubbles:true, cancelable:true})
  ev.enterev = new KeyboardEvent('keydown', {'key': 'Enter', "keyCode":10, bubbles:true, cancelable:true})
  ev.f4ev = new KeyboardEvent('keydown', {'key': 'F4', "keyCode":62, bubbles:true, cancelable:true})
  ev.mouseev = new MouseEvent('dblclick', {view:window, bubbles:true, cancelable:true})

  document.getElementById(elmId).dispatchEvent(ev[name])
}

export function wrapXHR(callback){
  let _XMLHttpRequest = XMLHttpRequest
  XMLHttpRequest = new_XMLHttpRequest

  if(!callback) callback = ()=>{}

  function new_XMLHttpRequest() {
    let xhr = new _XMLHttpRequest();
  
    xhr.onreadystatechange = ()=>{}
    let _open = xhr.open;
    let _send = xhr.send;
    let _url = '';

    xhr.open = function() {
      _url = arguments[1]
      return _open.apply(this, arguments);
    }
  
    xhr.send = function() {
      callback(arguments)
      return _send.apply(this, arguments);
    }
  
    return xhr;
  }
}

/*
BITMAP
*/

/*
BMP HEADER

0h	2	42 4D	"BM"	ID field (42h, 4Dh)
2h	4	46 00 00 00	70 bytes (54+16)	Size of the BMP file (54 bytes header + 16 bytes data)
6h	2	00 00	Unused	Application specific
8h	2	00 00	Unused	Application specific
Ah	4	36 00 00 00	54 bytes (14+40)	Offset where the pixel array (bitmap data) can be found
DIB Header
Eh	4	28 00 00 00	40 bytes	Number of bytes in the DIB header (from this point)
12h	4	02 00 00 00	2 pixels (left to right order)	Width of the bitmap in pixels
16h	4	02 00 00 00	2 pixels (bottom to top order)	Height of the bitmap in pixels. Positive for bottom to top pixel order.
1Ah	2	01 00	1 plane	Number of color planes being used
1Ch	2	18 00	24 bits	Number of bits per pixel
1Eh	4	00 00 00 00	0	BI_RGB, no pixel array compression used
22h	4	10 00 00 00	16 bytes	Size of the raw bitmap data (including padding)
26h	4	13 0B 00 00	2835 pixels/metre horizontal	Print resolution of the image,
72 DPI × 39.3701 inches per metre yields 2834.6472
2Ah	4	13 0B 00 00	2835 pixels/metre vertical
2Eh	4	00 00 00 00	0 colors	Number of colors in the palette
32h	4	00 00 00 00	0 important colors	0 means all colors are important
Start of pixel array (bitmap data)
36h	3	00 00 FF	0 0 255	Red, Pixel (0,1)
39h	3	FF FF FF	255 255 255	White, Pixel (1,1)
3Ch	2	00 00	0 0	Padding for 4 byte alignment (could be a value other than zero)
3Eh	3	FF 00 00	255 0 0	Blue, Pixel (0,0)
41h	3	00 FF 00	0 255 0	Green, Pixel (1,0)
44h	2	00 00	0 0	Padding for 4 byte alignment (could be a value other than zero)

octets multiple de 4
*/

export function toBMP(tt, nn=""){
	var vector;
	if(nn){
		vector = nn;
    if(vector.byteLength != 16) throw 'expect 16 bytes vector';
	} else {
		vector = new Uint8Array(16);
	}

	tt = str2buf(tt);

  var z;

  for(z=1;(tt.length+16)>z*4*z*32;z++);

	var w = z*32;
  // negative value, top to bottom
	var h = 256*256*256*256-w;
	var s = 62 + z*4*z*32;
	var u8 = new Uint8Array(s);

	// a=b=c=x=y=9;
	var header = [
    0x42,0x4D,
    (s>>>0&255),(s>>>8&255),(s>>>16&255),(s>>>24&255),
    (tt.length>>>0&255),(tt.length>>>8&255),(tt.length>>>16&255),(tt.length>>>24&255),
    0x3E,0,0,0,
    0x28,0,0,0,
    (w>>>0&255),(w>>>8&255),(w>>>16&255),(w>>>24&255),
    (h>>>0&255),(h>>>8&255),(h>>>16&255),(h>>>24&255),
    0x01,0,
    0x01,0,
    0,0,0,0,
    0,0,0,0,
    0,0,0,0,
    0,0,0,0,
    0,0,0,0,
    0,0,0,0,
    0xFF,0xFF,0xFF,
    0,0,0,0,0
	];

	u8.set(header);
	u8.set(vector,62);
	u8.set(tt,62+16);

  var rd = window.crypto.getRandomValues(new Uint8Array(s-62-16-tt.length));
	u8.set(rd,62+16+tt.length);

	// console.log(u8);
	// var bb = new Blob([u8]);
	// dl(bb,'aze.bmp');

	// xx=u8;
	return u8;
}

export function fromBMP(u8){
	if(!(u8 instanceof Uint8Array) || u8.length < 62+16) return u8;

	let len = u8[6] + (u8[7]<<8) + (u8[8]<<16) + (u8[9]<<24);
  let ret = {};
  ret.result = u8.slice(62+16, 62+16+len);
  ret.iv = u8.slice(62, 62+16);
	return ret;
}

/** STORE **/

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <script src="/js/0utils/simpledropzone.js"></script>
  <script>
(async function (){
  window._ = await import('/js/0utils/utils3.mjs')
  window.cr = await import('/js/0utils/simplewebcrypto.mjs')

  simpleDropzone("main")

  window.userData = null
  window.handleFiles = async function(files){
    if(!userData) userData = _.saveUserSettings('','localStorage') || {_part: 0, _tarObj: null, _filenames: []}

    let i, path, filenames, tmp

    files = await _.readfiles(files)
    filenames = Object.keys(files)
    path = document.getElementById('path').value
    for(i=0; i<filenames.length; i++){
      userData._filenames.push(filenames[i])
      userData._tarObj = _.tarAppend(userData._tarObj, [filenames[i], files[filenames[i]]])
    }
  }
})()
  </script>
</head>
<body>
  <center style="margin: 16px;"><input type="text" id="path" style="width: 100%; max-width: 480px;"></center>
  <div id="main"></div>
</body>

</html>