/**
 * app.js — 主界面逻辑
 */
import { parseMotionPhoto } from './motionphoto.js';
import { extractExifFromJpeg, readExifDate, extractExifMeta, buildExifMinimalAppleMakerNote } from './exif.js';
import { mp4ToLivePhotoMovMinimal } from './mov.js';

const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
const cardsEl = $('#cards');
const dropzone = $('#dropzone');
const fileInput = $('#fileInput');
const toolbar = $('#toolbar');
const fmtHint = $('#fmtHint');

if (fmtHint) {
  fmtHint.textContent = 'JPEG：不重新编码，画质完全无损、速度极快，Synology Photos 索引最稳';
}

/** 同步所有卡片上导出按钮的文案 */
function syncActionLabels() {
  $$('.act-pair').forEach(b => b.textContent = '导出 .pvt.zip');
}

/* ---------- toast ---------- */
function toast(msg, type = '') {
  const el = document.createElement('div');
  el.className = `toast ${type}`;
  el.textContent = msg;
  $('#toasts').appendChild(el);
  setTimeout(() => { el.style.opacity = '0'; el.style.transition = '.3s'; setTimeout(() => el.remove(), 350); }, 3200);
}

/* ---------- 下载 ---------- */
function download(bytes, filename, mime) {
  const blob = new Blob([bytes], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

function baseName(name) {
  return name.replace(/\.[^.]+$/, '');
}
function fmtSize(n) {
  if (n > 1024 * 1024) return (n / 1024 / 1024).toFixed(2) + ' MB';
  if (n > 1024) return (n / 1024).toFixed(1) + ' KB';
  return n + ' B';
}
function uuidUpper() {
  return (crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = Math.random() * 16 | 0;
    return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
  })).toUpperCase();
}

/* ---------- 文件处理 ---------- */
const items = []; // 每个上传文件一个条目

async function addFiles(files) {
  const jpgs = [...files].filter(f => /\.jpe?g$/i.test(f.name));
  const skipped = files.length - jpgs.length;
  if (skipped > 0) toast(`已跳过 ${skipped} 个非 JPG 文件`, 'err');
  if (jpgs.length === 0) return;
  toolbar.hidden = false;
  for (const f of jpgs) {
    await handleFile(f);
  }
}

async function handleFile(file) {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const item = {
    id: items.length,
    file,
    name: file.name,
    bytes,
    parsed: null,
    exifTiff: null,
    photoDate: null,
    imageBitmapInfo: null, // {width, height}
    el: null,
    busy: false,
  };
  items.push(item);
  renderCard(item);

  const parsed = parseMotionPhoto(bytes);
  item.parsed = parsed;

  if (!parsed.isLivePhoto) {
    // 非动态照片：整个文件作为图片，仍支持导出 JPEG
    parsed.image = bytes;
    item.parsed = parsed;
    updateCard(item, {
      state: 'err', stateText: '非动态照片（不含视频）',
      errorMsg: parsed.reason + '。若这是华为动态照片，请改用「文件管理」App 选择相册原始目录（DCIM/Camera）里的 .jpg 原图——相册/图库入口会返回去掉视频的静态缩略图。',
      imgURL: URL.createObjectURL(new Blob([bytes], { type: 'image/jpeg' })),
      meta: { '文件大小': fmtSize(bytes.length) }
    });
    try {
      const bmp = await createImageBitmap(new Blob([bytes], { type: 'image/jpeg' }), { imageOrientation: 'from-image' });
      item.exifTiff = extractExifFromJpeg(bytes);
      item.photoDate = readExifDate(item.exifTiff);
      const m = item.el.querySelector('.meta');
      m.innerHTML += `<div>图片: <b>${bmp.width} × ${bmp.height}</b></div>`;
      bmp.close && bmp.close();
    } catch (e) { /* ignore */ }
    return;
  }

  item.exifTiff = extractExifFromJpeg(parsed.image);
  item.exifMeta = extractExifMeta(item.exifTiff);
  item.photoDate = readExifDate(item.exifTiff);

  // 预览
  const imgBlob = new Blob([parsed.image], { type: 'image/jpeg' });
  const imgURL = URL.createObjectURL(imgBlob);
  const vidURL = URL.createObjectURL(new Blob([parsed.video], { type: 'video/mp4' }));
  item.imgURL = imgURL;
  item.vidURL = vidURL;

  try {
    const bmp = await createImageBitmap(imgBlob, { imageOrientation: 'from-image' });
    item.imageBitmapInfo = { width: bmp.width, height: bmp.height };
    bmp.close && bmp.close();
  } catch (e) {
    item.imageBitmapInfo = null;
  }

  const videoEl = item.el.querySelector('video');
  videoEl.addEventListener('loadedmetadata', () => {
    item.videoDuration = videoEl.duration;
    const durEl = item.el.querySelector('.v-dur');
    if (durEl) durEl.textContent = videoEl.duration.toFixed(1) + ' 秒';
  });

  updateCard(item, {
    state: 'ok', stateText: '解析成功',
    imgURL, vidURL,
    meta: {
      '格式': parsed.format,
      '图片': item.imageBitmapInfo ? `${item.imageBitmapInfo.width} × ${item.imageBitmapInfo.height}` : '—',
      '图片大小': fmtSize(parsed.image.length),
      '视频大小': fmtSize(parsed.video.length),
      '视频时长': '<span class="v-dur">…</span>',
      '拍摄时间': item.photoDate ? item.photoDate.toLocaleString('zh-CN') : '（无 EXIF）',
    }
  });
}

/* ---------- 渲染 ---------- */
function renderCard(item) {
  const el = document.createElement('div');
  el.className = 'card';
  el.innerHTML = `
    <div class="card-top">
      <div class="preview">
        <div class="tabs">
          <button class="tab-btn active" data-tab="img">图片</button>
          <button class="tab-btn" data-tab="vid">视频</button>
        </div>
        <img alt="照片预览" style="display:none">
        <video controls playsinline style="display:none"></video>
      </div>
      <div class="card-body">
        <div class="card-title">
          <h3></h3>
          <span class="state busy">解析中…</span>
        </div>
        <div class="meta"></div>
        <div class="actions">
          <button class="btn small ghost act-jpg">下载 JPG</button>
          <button class="btn small ghost act-mp4">下载 MP4</button>
          <button class="btn small ghost act-frame">截取视频当前帧</button>
          <div class="divider"></div>
          <button class="btn small primary act-pair">导出 .pvt.zip</button>
        </div>
        <div class="progress-note" style="display:none"></div>
        <div class="err-msg" style="display:none"></div>
      </div>
    </div>`;
  cardsEl.appendChild(el);
  item.el = el;

  el.querySelector('h3').textContent = item.name;

  // tab 切换
  const imgEl = el.querySelector('img');
  const vidEl = el.querySelector('video');
  el.querySelectorAll('.tab-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      el.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b === btn));
      const showImg = btn.dataset.tab === 'img';
      imgEl.style.display = showImg ? '' : 'none';
      vidEl.style.display = showImg ? 'none' : '';
      if (!showImg) vidEl.play().catch(() => {});
      else vidEl.pause();
    });
  });

  // 操作
  el.querySelector('.act-jpg').addEventListener('click', () => {
    if (!item.parsed || !item.parsed.isLivePhoto) return toast('该文件没有可用的图片部分', 'err');
    download(item.parsed.image, baseName(item.name) + '_photo.jpg', 'image/jpeg');
  });
  el.querySelector('.act-mp4').addEventListener('click', () => {
    if (!item.parsed || !item.parsed.isLivePhoto) return toast('该文件没有可用的视频部分', 'err');
    download(item.parsed.video, baseName(item.name) + '_video.mp4', 'video/mp4');
  });
  el.querySelector('.act-frame').addEventListener('click', () => captureFrame(item));
  el.querySelector('.act-pair').addEventListener('click', () => exportPair(item));
  syncActionLabels();
}

function updateCard(item, { state, stateText, imgURL, vidURL, meta, errorMsg }) {
  const el = item.el;
  if (state) {
    const s = el.querySelector('.state');
    s.className = `state ${state}`;
    s.textContent = stateText;
  }
  if (imgURL) el.querySelector('img').src = imgURL, el.querySelector('img').style.display = '';
  if (vidURL) el.querySelector('video').src = vidURL;
  if (meta) {
    const metaEl = el.querySelector('.meta');
    metaEl.innerHTML = Object.entries(meta).map(([k, v]) => `<div>${k}: <b>${v}</b></div>`).join('');
  }
  if (errorMsg !== undefined) {
    const e = el.querySelector('.err-msg');
    if (errorMsg) { e.textContent = errorMsg; e.style.display = ''; }
    else e.style.display = 'none';
  }
}

function setBusy(item, busy, note) {
  item.busy = busy;
  const n = item.el.querySelector('.progress-note');
  if (note) { n.textContent = note; n.style.display = ''; }
  else n.style.display = 'none';
  item.el.querySelectorAll('.actions .btn').forEach(b => b.disabled = busy);
  const s = item.el.querySelector('.state');
  if (busy) { item._oldState = s.textContent; s.className = 'state busy'; s.textContent = '处理中…'; }
  else if (item._oldState !== undefined) {
    s.className = 'state ' + (item.parsed && item.parsed.isLivePhoto ? 'ok' : 'err');
    s.textContent = item._oldState;
  }
}

/* ---------- 导出动作 ---------- */

/**
 * 导出 JPEG 静态图：在原始 JPEG 的 EXIF 中注入 Apple MakerNote，**不重新编码**。
 * Apple 的实况配对只看 ContentIdentifier，因此这条路径画质完全无损、速度极快，
 * 且对第三方照片库（Synology Photos）兼容性更好。
 */
function exportJpeg(item, uuid) {
  if (!item.parsed) return toast('文件尚未解析完成', 'err');
  setBusy(item, true, '正在写入配对标识…');
  try {
    const uid = uuid || uuidUpper();
    // 精简模式：重建 EXIF 为 Synology 实测可识别参照同构
    //   - IFD0 仅 Orientation + 原图 Model/Software（原值）+（可选拍摄时间 ModifyDate + GPS），不写 Make
    //   - ExifIFD 仅 ExifVersion + 拍摄参数 + 极简 Apple MakerNote（仅 ContentIdentifier）
    const jpeg = patchJpegWithMinimalExif(item.parsed.image, uid);
    if (!jpeg) {
      setBusy(item, false);
      toast('该 JPEG 未包含 Exif 段，无法写入配对标识', 'err');
      return null;
    }
    // 不单独下载 JPG，仅保留内存字节供 .pvt.zip 打包
    item._lastPhotoBytes = jpeg;
    setBusy(item, false);
    return uid;
  } catch (e) {
    setBusy(item, false);
    updateCard(item, { errorMsg: 'JPEG 导出失败：' + e.message });
    toast('JPEG 导出失败：' + e.message, 'err');
    return null;
  }
}

/**
 * 给 JPEG 重建精简 EXIF 段（Synology 实测可识别参照同构）。
 * 不是"注入"而是"重建"——不写 Make 字段（既不伪装 Apple 也不保留 HUAWEI），
 * 保留 Orientation + 原图 Model/Software +（可选拍摄时间 ModifyDate + GPS）
 * + 拍摄参数 + ExifVersion + 极简 MakerNote。
 */
function patchJpegWithMinimalExif(jpeg, uuid) {
  const EXIF_SIG = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00];
  let p = 2;
  while (p + 4 <= jpeg.length) {
    if (jpeg[p] !== 0xFF) { p++; continue; }
    const marker = jpeg[p + 1];
    if (marker === 0x01 || (marker >= 0xD0 && marker <= 0xD9)) { p += 2; continue; }
    if (marker === 0xDA) break;
    const segLen = (jpeg[p + 2] << 8) | jpeg[p + 3];
    if (segLen < 2) break;
    let isExif = marker === 0xE1;
    if (isExif) {
      for (let i = 0; i < 6; i++) {
        if (jpeg[p + 4 + i] !== EXIF_SIG[i]) { isExif = false; break; }
      }
    }
    if (isExif) {
      const tiffStart = p + 10;
      const segEnd = p + 2 + segLen;
      const tiff = jpeg.slice(tiffStart, segEnd);
      const newTiff = buildExifMinimalAppleMakerNote(tiff, uuid);
      const newSegLen = 2 + 6 + newTiff.length;
      if (newSegLen > 0xFFFF) throw new Error('EXIF 段超过 64KB，无法写入 JPEG');
      const out = new Uint8Array(jpeg.length - segLen + newSegLen);
      out.set(jpeg.subarray(0, p), 0);
      let o = p;
      out[o++] = 0xFF; out[o++] = 0xE1;
      out[o++] = (newSegLen >> 8) & 255; out[o++] = newSegLen & 255;
      out.set(EXIF_SIG, o); o += 6;
      out.set(newTiff, o); o += newTiff.length;
      out.set(jpeg.subarray(segEnd), o);
      return out;
    }
    p += 2 + segLen;
  }
  return null;
}

/** 导出静态图（固定 JPEG） */
async function exportPhoto(item, uuid) {
  return exportJpeg(item, uuid);
}

function exportMov(item, uuid) {
  if (!item.parsed || !item.parsed.isLivePhoto) return toast('该文件没有可用的视频部分', 'err');
  try {
    const uid = uuid || uuidUpper();
    // 精简模式：注入 ContentIdentifier（makelive 兼容）；可选写入拍摄时间(mvhd)与地点(location key)
    const r = mp4ToLivePhotoMovMinimal(item.parsed.video, uid, item.photoDate, {
      exifMeta: item.exifMeta || {},
    });
    const bytes = r.bytes;
    if (r.warnings && r.warnings.length) console.warn('[minimal MP4]', r.warnings);
    // 不单独下载 MP4，仅保留内存字节供 .pvt.zip 打包
    item._lastMovBytes = bytes;
    return uid;
  } catch (e) {
    toast('视频转换失败：' + e.message, 'err');
    return null;
  }
}

async function exportPair(item) {
  if (!item.parsed || !item.parsed.isLivePhoto) {
    // 非动态照片：仅导出静态图
    return exportPhoto(item);
  }
  const uid = uuidUpper();
  const okPhoto = await exportPhoto(item, uid);
  if (okPhoto !== uid) return; // 静态图失败则不再导出，避免产生无法配对的孤儿文件
  exportMov(item, uid);
  // 只输出 .pvt.zip（makelivephoto.com 同款 iOS 格式，AirDrop 到 iPhone 直接识别为实况照片）
  exportPvt(item, uid, 'JPEG');
}

/**
 * 生成并下载「.pvt」包（makelivephoto.com 同款 iOS 格式）。
 * .pvt 是 macOS/iOS 识别 Live Photo 的私有目录：内部含同名 <base>.jpg + <base>.mp4
 * + metadata.plist。用户把它 AirDrop 到 iPhone 即被识别为实况照片（无需第三方 App）。
 *
 * 浏览器无法直接写出目录，故打包为 ZIP（store 不压缩），结构：
 *   <base>.pvt/
 *     <base>.jpg
 *     <base>.mp4
 *     metadata.plist
 * 用户解压后得到 <base>.pvt 文件夹，对其右键/拖入 AirDrop 即可。
 */
function exportPvt(item, uid, what) {
  try {
    const base = baseName(item.name);
    const photoExt = 'jpg';
    const photoName = base + '.' + photoExt;
    // 精简模式视频是 .mp4（mp42/isom），与 makelivephoto.com 参照一致
    const movName = base + '.mp4';
    // 复用已导出的内存产物：重新生成一遍（保持简单、确定）
    const photoBytes = item._lastPhotoBytes || null;
    const movBytes = item._lastMovBytes || null;
    if (!photoBytes || !movBytes) {
      console.warn('[.pvt] 缺少内存产物，跳过');
      return;
    }
    // 与 makelivephoto.com 参照逐字节同构：版本号用 <string>1</string>（非 <integer>）
    const plist =
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>PFVideoComplementMetadataVersionKey</key><string>1</string></dict></plist>
`;
    // 使用说明（与参照一致，放在 ZIP 顶层）
    const readme =
`# 如何导入到 iPhone？

下载 ZIP 并解压后，.pvt 文件在苹果系统中会被自动识别为 Live 图，您可以通过以下两种方式同步到 iPhone：

方式一：AirDrop
- 把 .pvt 文件 AirDrop 到 iPhone
- iPhone 会在相册中自动识别为 Live Photo

方式二：iCloud 同步
- 在 macOS 设备上双击 .pvt 文件，自动导入到“照片”App
- 通过 iCloud Photos 同步到 iPhone

# 导入到 Synology Photos
- 将解压后的同名 .jpg 与 .mp4 通过 Synology Photos 移动 App 上传（不要用网页/File Station）
- 两者通过相同的 ContentIdentifier 自动配对为动态照片
`;
    const zip = buildStoreZip([
      { name: base + '.pvt/', bytes: new Uint8Array(0) }, // 目录项（与参照一致）
      { name: base + '.pvt/' + photoName, bytes: photoBytes },
      { name: base + '.pvt/' + movName, bytes: movBytes },
      { name: base + '.pvt/metadata.plist', bytes: new TextEncoder().encode(plist) },
      { name: '使用说明.txt', bytes: new TextEncoder().encode(readme) },
    ]);
    download(zip, base + '.pvt.zip', 'application/zip');
    toast('已附随下载 .pvt.zip（解压后 AirDrop 到 iPhone / 或上传同名 jpg+mp4 到 Synology Photos）', 'ok');
  } catch (e) {
    console.warn('[.pvt] 生成失败', e);
  }
}

/**
 * 极简 store-only ZIP 编码器（无压缩，足够打包 .pvt）。
 * 严格按 APPNOTE ZIP 规范写字段：2 字节字段用 u16le，4 字节字段用 u32le。
 * 早期版本误把所有字段都写成 u32le（4 字节），导致中央目录偏移错乱、
 * python zipfile 报 "Truncated central directory"。此处已修正字段宽度。
 */
function buildStoreZip(entries) {
  const enc = new TextEncoder();
  const chunks = [];
  let offset = 0;
  const central = [];
  for (const e of entries) {
    const nameBytes = enc.encode(e.name);
    const data = e.bytes instanceof Uint8Array ? e.bytes : new Uint8Array(e.bytes);
    const crc = crc32(data);
    // ZIP 通用标志位：bit 11 (0x0800) = UTF-8 文件名编码。
    // 参照 makelivephoto.com 产物即设置此位（Python/Windows 才能正确显示中文文件名），
    // 仅当文件名含非 ASCII 字节时设置，对纯 ASCII 名无副作用。
    let utf8 = 0;
    for (let i = 0; i < nameBytes.length; i++) {
      if (nameBytes[i] >= 0x80) { utf8 = 0x0800; break; }
    }
    // Local file header: sig(4) + ver(2) + flags(2) + method(2) + time(2) + date(2)
    //                  + crc(4) + csize(4) + usize(4) + nlen(2) + elen(2) + name + data
    const local = [
      ...u32le(0x04034b50),
      ...u16le(20), ...u16le(utf8),                 // version needed, flags (含 UTF-8 位)
      ...u16le(0), ...u16le(0), ...u16le(0),        // method=0, modtime, moddate
      ...u32le(crc), ...u32le(data.length), ...u32le(data.length),
      ...u16le(nameBytes.length), ...u16le(0),      // name length, extra length
      ...nameBytes, ...data
    ];
    chunks.push(Uint8Array.from(local));
    // Central directory header: sig(4) + vb(2) + vn(2) + flags(2) + method(2)
    //   + time(2) + date(2) + crc(4) + csize(4) + usize(4) + nlen(2) + elen(2)
    //   + clen(2) + dsk(2) + iattr(2) + eattr(4) + lho(4) + name
    const cen = [
      ...u32le(0x02014b50),
      ...u16le(20), ...u16le(20), ...u16le(utf8),   // version made by, version needed, flags (同步 UTF-8 位)
      ...u16le(0), ...u16le(0), ...u16le(0),        // method, modtime, moddate
      ...u32le(crc), ...u32le(data.length), ...u32le(data.length),
      ...u16le(nameBytes.length), ...u16le(0), ...u16le(0), ...u16le(0), // nlen, elen, clen, dsk
      ...u16le(0), ...u32le(0), ...u32le(offset),   // internal attrs, external attrs, local header offset
      ...nameBytes
    ];
    central.push(Uint8Array.from(cen));
    offset += local.length;
  }
  let centralSize = 0;
  for (const c of central) centralSize += c.length;
  const centralOffset = offset;
  // End of central directory: sig(4) + dsk(2) + cdsk(2) + nrec(2) + ntot(2)
  //   + csize(4) + coff(4) + clen(2)
  const tail = [
    ...u32le(0x06054b50),
    ...u16le(0), ...u16le(0),                       // this disk, disk with cd start
    ...u16le(entries.length), ...u16le(entries.length), // records on disk, total records
    ...u32le(centralSize), ...u32le(centralOffset),
    ...u16le(0)                                     // comment length
  ];
  const all = [...chunks, ...central, Uint8Array.from(tail)];
  const total = all.reduce((s, a) => s + a.length, 0);
  const out = new Uint8Array(total);
  let p = 0;
  for (const a of all) { out.set(a, p); p += a.length; }
  return out;
}
function u32le(v) {
  return [v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff];
}
function u16le(v) {
  return [v & 0xff, (v >>> 8) & 0xff];
}
function crc32(buf) {
  let c = ~0;
  for (let i = 0; i < buf.length; i++) {
    c ^= buf[i];
    for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1));
  }
  return (~c) >>> 0;
}


function captureFrame(item) {
  if (!item.parsed || !item.parsed.isLivePhoto) return toast('该文件没有可用的视频部分', 'err');
  const video = item.el.querySelector('video');
  if (!video.videoWidth) return toast('请先切换到「视频」标签并等待视频加载', 'err');
  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth; canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video, 0, 0);
  canvas.toBlob(blob => {
    if (!blob) return toast('截帧失败', 'err');
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    const t = video.currentTime.toFixed(2).replace('.', '_');
    a.download = `${baseName(item.name)}_frame_${t}s.jpg`;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 4000);
    toast('已截取当前帧为 JPG', 'ok');
  }, 'image/jpeg', 0.95);
}

/* ---------- 批量 ---------- */
$('#exportAllBtn').addEventListener('click', async () => {
  const liveItems = items.filter(it => it.parsed && it.parsed.isLivePhoto && !it.busy);
  if (liveItems.length === 0) return toast('列表中没有已解析的动态照片', 'err');
  toast(`开始批量导出 ${liveItems.length} 组 Live Photo…`);
  for (const it of liveItems) {
    await exportPair(it);
  }
});

$('#clearBtn').addEventListener('click', () => {
  items.forEach(it => {
    if (it.imgURL) URL.revokeObjectURL(it.imgURL);
    if (it.vidURL) URL.revokeObjectURL(it.vidURL);
  });
  items.length = 0;
  cardsEl.innerHTML = '';
  toolbar.hidden = true;
});

/* ---------- 拖拽 ---------- */
dropzone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => { addFiles(fileInput.files); fileInput.value = ''; });
['dragenter', 'dragover'].forEach(ev => dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.classList.add('over'); }));
['dragleave', 'drop'].forEach(ev => dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.classList.remove('over'); }));
dropzone.addEventListener('drop', e => { if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); });
document.addEventListener('dragover', e => e.preventDefault());
document.addEventListener('drop', e => e.preventDefault());
