













(function () {
  'use strict';

  var T = function (k, d) { return VizI18n ? VizI18n.tr(k, d) : d; };

  var el = {};              
  var opts = null;          
  var view = 'auth';        
  var mode = 'open';        
  var auth = { state: 'logged_out' };
  var files = [];
  var listing = false;      
  var quota = 0, used = 0;
  var selectedId = null;
  var currentId = null;     
  var pendingDoc = null;    
  var confirmDelId = null;  
  var conflict = null;      
  var busy = null;          
  var savedNote = '';       
  


  var startingLogin = false;

  

  function sizeText(n) {
    n = Number(n) || 0;
    if (n >= 1048576) return (n / 1048576).toFixed(2) + ' MB';
    if (n >= 1024) return (n / 1024).toFixed(1) + ' KB';
    return n + ' B';
  }

  function timeText(sec) {
    if (!sec) return '';
    var d = new Date(Number(sec) * 1000);
    var p = function (x) { return (x < 10 ? '0' : '') + x; };
    return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
           ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
  }

  function mk(tag, cls, text) {
    var e = document.createElement(tag);
    if (cls) e.className = cls;
    if (text !== undefined && text !== null) e.textContent = text;
    return e;
  }

  

  function injectStyle() {
    var css = [
      '#cloud-backdrop{position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.45);display:none;}',
      
















      '#cloud-pop{position:fixed;top:0;right:0;bottom:0;left:0;',
      '  margin:auto;height:fit-content;',
      '  max-height:92vh;overflow:auto;z-index:40;',
      '  width:460px;max-width:92vw;background:rgba(28,31,36,.98);border:1px solid #4da3ff;',
      '  border-radius:6px;box-shadow:0 6px 18px rgba(0,0,0,.5);display:none;',
      '  font-size:12px;color:#c5c8c6;}',
      '#cloud-pop h3{margin:0;padding:12px 14px 8px;font-size:13px;color:#ffcc44;font-weight:600;}',
      '#cloud-pop .who{padding:0 14px 8px;color:#8f979e;font-size:11px;}',
      '#cloud-pop .body{padding:0 14px;}',
      '#cloud-pop .msg{margin:0 0 10px;line-height:1.6;white-space:pre-line;}',
      '#cloud-pop .err{color:#ff9a90;}',
      '#cloud-pop .okline{color:#5fcf80;}',
      '#cloud-pop .code{margin:12px 0 6px;text-align:center;font-size:26px;letter-spacing:6px;',
      '  color:#4da3ff;font-weight:600;font-family:Consolas,Monaco,monospace;}',
      '#cloud-pop .bar{height:6px;background:#23272c;border-radius:4px;overflow:hidden;margin:2px 0 4px;}',
      '#cloud-pop .bar i{display:block;height:100%;background:#4da3ff;}',
      '#cloud-pop .bar.full i{background:#c55;}',
      '#cloud-pop .quota{color:#8f979e;font-size:11px;margin-bottom:8px;}',
      '#cloud-pop .list{max-height:46vh;overflow-y:auto;border:1px solid #47505a;border-radius:4px;}',
      '#cloud-pop .row{display:flex;align-items:center;gap:8px;padding:7px 9px;cursor:pointer;',
      '  border-bottom:1px solid rgba(71,80,90,.45);}',
      '#cloud-pop .row:last-child{border-bottom:0;}',
      '#cloud-pop .row:hover{background:#23272c;}',
      '#cloud-pop .row.on{background:#2d5f8b;color:#fff;}',
      '#cloud-pop .row .nm{flex:1 1 auto;word-break:break-all;}',
      '#cloud-pop .row .cur{color:#ffcc44;font-size:10px;margin-left:6px;}',
      


      '#cloud-pop .row .meta{flex:0 0 auto;color:#8f979e;font-size:11px;font-variant-numeric:tabular-nums;}',
      '#cloud-pop .row .sz{flex:0 0 62px;text-align:right;}',
      '#cloud-pop .row .tm{flex:0 0 106px;text-align:right;padding-left:14px;}',
      '#cloud-pop .row.on .meta{color:#cfe3f7;}',
      



      '#cloud-pop .row .ops{flex:0 0 110px;display:flex;gap:6px;justify-content:flex-end;}',
      '#cloud-pop .row .ops.confirm{flex:0 0 auto;}',
      
      '#cloud-pop .hd{display:flex;align-items:center;gap:8px;padding:0 9px 4px;',
      '  color:#7d858c;font-size:10.5px;}',
      '#cloud-pop .hd .nm{flex:1 1 auto;}',
      '#cloud-pop .hd .sz{flex:0 0 62px;text-align:right;}',
      '#cloud-pop .hd .tm{flex:0 0 106px;text-align:right;padding-left:14px;}',
      '#cloud-pop .hd .ops{flex:0 0 110px;}',
      
      '#cloud-pop.wide{width:620px;}',
      '#cloud-pop .row .ops button{padding:2px 7px;font-size:11px;}',
      '#cloud-pop .row .ops .del{color:#ff9a90;}',
      '#cloud-pop .empty{padding:26px 10px;text-align:center;color:#8f979e;line-height:1.7;}',
      '#cloud-pop .nameline{display:flex;align-items:center;gap:8px;margin:6px 0 10px;}',
      '#cloud-pop .nameline input{flex:1 1 auto;background:#23272c;color:#c5c8c6;border:1px solid #47505a;',
      '  border-radius:4px;padding:5px 7px;font-size:12px;}',
      '#cloud-pop .hint{color:#8f979e;font-size:11px;line-height:1.5;margin:0 0 10px;}',
      '#cloud-pop .btns{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px 12px;}',
      '#cloud-pop .btns .spacer{flex:1 1 auto;}',
      '#cloud-pop .btns .go{background:#2d5f8b;border-color:#4da3ff;color:#fff;}',
      '#cloud-pop .btns .go:hover{background:#3a76ab;}',
      '#cloud-pop .btns .go:disabled{background:#2b3138;border-color:#47505a;color:#8f979e;}',
      '#cloud-pop .btns .link{background:none;border:0;color:#8f979e;}',
      '#cloud-pop .btns .link:hover{color:#c5c8c6;}',
      
      '#cloud-pop .url{margin:2px 0 10px;padding:6px 8px;background:#23272c;border:1px solid #47505a;',
      '  border-radius:4px;font-family:Consolas,Monaco,monospace;font-size:11px;color:#9fd0ff;',
      '  word-break:break-all;user-select:text;cursor:text;}',
      '#cloud-pop .wait{color:#8f979e;font-size:11px;text-align:center;margin:0 0 6px;}',
      


      '#cloud-pop .bar.indet i{width:35%;animation:cloud-indet 1.1s ease-in-out infinite;}',
      '@keyframes cloud-indet{0%{transform:translateX(0);}50%{transform:translateX(186%);}'
      + '100%{transform:translateX(0);}}'
    ].join('\n');
    var s = document.createElement('style');
    s.textContent = css;
    document.head.appendChild(s);
  }

  function build() {
    injectStyle();
    el.backdrop = mk('div');
    el.backdrop.id = 'cloud-backdrop';
    el.backdrop.addEventListener('click', close);

    el.pop = mk('div');
    el.pop.id = 'cloud-pop';
    el.title = mk('h3', null, '');
    el.who = mk('div', 'who', '');
    el.body = mk('div', 'body');
    el.btns = mk('div', 'btns');
    el.pop.appendChild(el.title);
    el.pop.appendChild(el.who);
    el.pop.appendChild(el.body);
    el.pop.appendChild(el.btns);

    document.body.appendChild(el.backdrop);
    document.body.appendChild(el.pop);

    
    document.addEventListener('keydown', function (ev) {
      if (!isOpen()) return;
      if (ev.key === 'Escape') { ev.stopPropagation(); ev.preventDefault(); close(); }
    }, true);
  }

  function isOpen() { return el.pop && el.pop.style.display === 'block'; }

  function show() {
    el.backdrop.style.display = 'block';
    el.pop.style.display = 'block';
    render();
  }

  function close() {
    if (!el.pop) return;
    






    if (view === 'auth' && (auth.state === 'pending' || startingLogin) && VizChannel.cloud) {
      VizChannel.cloud.cancel_login();
    }
    startingLogin = false;
    el.backdrop.style.display = 'none';
    el.pop.style.display = 'none';
    confirmDelId = null;
    pendingDoc = null;
    busy = null;
    listing = false;      
    savedNote = '';
  }

  

  function render() {
    if (!isOpen()) return;
    el.body.innerHTML = '';
    el.btns.innerHTML = '';
    el.who.textContent = '';
    
    el.pop.className = (view === 'list' && files.length) ? 'wide' : '';
    if (view === 'auth') renderAuth();
    else if (view === 'name') renderName();
    else if (view === 'conflict') renderConflict();
    else if (view === 'busy') renderBusy();
    else renderList();
  }

  

  function renderBusy() {
    var up = busy && busy.phase === 'upload';
    var total = (busy && busy.total) || 0;
    var done = (busy && busy.done) || 0;
    


    var awaiting = up && total > 0 && done >= total;
    el.title.textContent = up
      ? T('viz_cloud_save_title', '保存到云端')
      : T('viz_cloud_open_title', '打开云端场景');
    el.body.appendChild(mk('div', 'msg',
      (awaiting ? T('viz_cloud_awaiting', '已发送完毕，等待服务器确认…')
        : (up ? T('viz_cloud_uploading', '正在保存「{name}」…')
              : T('viz_cloud_downloading', '正在下载「{name}」…')))
      .replace('{name}', (busy && busy.name) || '')));
    
    var indet = !(total > 0) || awaiting;
    var bar = mk('div', 'bar' + (indet ? ' indet' : ''));
    var fill = mk('i');
    if (!indet) fill.style.width = Math.min(100, Math.round(done * 100 / total)) + '%';
    bar.appendChild(fill);
    el.body.appendChild(bar);
    if (total > 0 && !awaiting) {
      el.body.appendChild(mk('div', 'quota', sizeText(done) + ' / ' + sizeText(total)));
    }
    el.btns.appendChild(mk('span', 'spacer'));
    
    addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
  }

  function addBtn(parent, label, cls, onClick, disabled) {
    var b = mk('button', cls || '', label);
    if (disabled) b.disabled = true;
    b.addEventListener('click', onClick);
    parent.appendChild(b);
    return b;
  }

  function renderAuth() {
    


    if (startingLogin && auth.state !== 'pending') {
      el.title.textContent = T('viz_cloud_login_title', '登录 enpeicv.com');
      el.body.appendChild(mk('div', 'msg',
        T('viz_cloud_login_starting', '正在获取授权码…')));
      el.body.appendChild(mk('div', 'bar indet')).appendChild(mk('i'));
      el.btns.appendChild(mk('span', 'spacer'));
      addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
      return;
    }
    if (auth.state === 'pending') {
      el.title.textContent = T('viz_cloud_login_title', '登录 enpeicv.com');
      


      var ok = auth.browser_ok !== false;
      el.body.appendChild(mk('div', ok ? 'msg' : 'msg err',
        ok ? T('viz_cloud_login_pending',
               '已在浏览器里打开授权页。用你的论坛账号登录后点「同意授权」即可。')
           : T('viz_cloud_login_manual',
               '浏览器没能自动打开。请复制下面的链接，自己在浏览器里打开：')));
      
      el.body.appendChild(mk('div', 'url', auth.verify_url || ''));
      el.body.appendChild(mk('div', 'code', auth.user_code || '------'));
      el.body.appendChild(mk('div', 'hint', T('viz_cloud_login_code_hint',
        '网页上会显示同样的核对码。对不上就说明那个页面不是这里打开的，请直接关掉。')));
      if (auth.wait_left_s) {
        el.body.appendChild(mk('div', 'wait', T('viz_cloud_login_wait', '还可以等 {s} 秒')
          .replace('{s}', auth.wait_left_s)));
      }
      



      if (auth.message) {
        el.body.appendChild(mk('div', 'msg err', auth.message));
      }
      
      
      addBtn(el.btns, T('viz_cloud_copy_link', '复制链接'), ok ? 'link' : 'go', function () {
        if (VizChannel.cloud) VizChannel.cloud.copy_verify_url();
      });
      addBtn(el.btns, T('viz_cloud_login_reopen', '重新打开浏览器'), 'link', function () {
        if (VizChannel.cloud) VizChannel.cloud.reopen_browser();
      });
      el.btns.appendChild(mk('span', 'spacer'));
      addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);   
      return;
    }
    el.title.textContent = T('viz_cloud_login_title', '登录 enpeicv.com');
    el.body.appendChild(mk('div', 'msg', T('viz_cloud_login_intro',
      '用论坛账号登录后，场景就能存到云端，换台电脑也能接着用。\n登录在浏览器里完成，密码不经过本程序。')));
    if (auth.message) {
      el.body.appendChild(mk('div', 'msg err', auth.message));
    }
    el.btns.appendChild(mk('span', 'spacer'));
    addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
    addBtn(el.btns, T('viz_cloud_login_go', '登录'), 'go', function () {
      if (VizChannel.cloud) VizChannel.cloud.login();
    });
  }

  function renderName() {
    el.title.textContent = T('viz_cloud_save_title', '保存到云端');
    var line = mk('div', 'nameline');
    var input = mk('input');
    input.type = 'text';
    input.value = (pendingDoc && pendingDoc.name) || 'scene';
    input.maxLength = 60;
    line.appendChild(mk('span', null, T('viz_cloud_name_label', '名称')));
    line.appendChild(input);
    el.body.appendChild(line);
    el.body.appendChild(mk('div', 'hint', T('viz_cloud_save_hint',
      '存在你的 enpeicv.com 账号下，只有你自己能看到。')));
    var doSave = function () {
      var n = (input.value || '').trim();
      if (!n) { input.focus(); return; }
      if (!VizChannel.cloud) { close(); return; }
      var doc = pendingDoc;
      toBusy('upload', n);                     
      VizChannel.cloud.save_scene(JSON.stringify({ doc: doc, name: n, as_new: true }));
    };
    input.addEventListener('keydown', function (ev) {
      if (ev.key === 'Enter') { ev.preventDefault(); doSave(); }
    });
    el.btns.appendChild(mk('span', 'spacer'));
    addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
    addBtn(el.btns, T('viz_cloud_save', '保存'), 'go', doSave);
    setTimeout(function () { input.focus(); input.select(); }, 0);
  }

  function renderList() {
    el.title.textContent = mode === 'save'
      ? T('viz_cloud_save_title', '保存到云端')
      : T('viz_cloud_open_title', '打开云端场景');
    el.who.textContent = (auth.username || '') +
      (auth.uid ? '（' + T('viz_cloud_uid', 'UID {uid}').replace('{uid}', auth.uid) + '）' : '') +
      (quota ? '　' + T('viz_cloud_quota', '已用 {used} / 共 {quota}')
                  .replace('{used}', sizeText(used)).replace('{quota}', sizeText(quota)) : '');

    var pct = quota > 0 ? Math.min(100, Math.round(used * 100 / quota)) : 0;
    var bar = mk('div', 'bar' + (pct >= 100 ? ' full' : ''));
    var fill = mk('i');
    fill.style.width = pct + '%';
    bar.appendChild(fill);
    el.body.appendChild(bar);

    if (savedNote) {
      el.body.appendChild(mk('div', 'msg okline',
        T('viz_cloud_saved_note', '✓ 已保存：{name}').replace('{name}', savedNote)));
    }
    if (auth.error) {
      el.body.appendChild(mk('div', 'msg err', auth.error));
    }

    












    var list = mk('div', 'list');
    if (listing) {
      var wait = mk('div', 'empty');
      wait.appendChild(mk('div', null,
        T('viz_cloud_listing', '正在加载云端场景列表…')));
      var lbar = mk('div', 'bar indet');
      lbar.style.marginTop = '12px';
      lbar.appendChild(mk('i'));
      wait.appendChild(lbar);
      list.appendChild(wait);
    } else if (auth.error) {
      list = null;                    
    } else if (!files.length) {
      list.appendChild(mk('div', 'empty', T('viz_cloud_empty',
        '还没有云端场景。\n按 Ctrl+S 就能把当前场景存上来。')));
    } else {
      

      var hd = mk('div', 'hd');
      hd.appendChild(mk('span', 'nm', T('viz_cloud_name_label', '名称')));
      hd.appendChild(mk('span', 'sz', T('viz_cloud_col_size', '大小')));
      hd.appendChild(mk('span', 'tm', T('viz_cloud_col_created', '创建时间')));
      hd.appendChild(mk('span', 'tm', T('viz_cloud_col_updated', '最近更新')));
      hd.appendChild(mk('span', 'ops', ''));
      el.body.appendChild(hd);
      files.forEach(function (f) { list.appendChild(renderRow(f)); });
    }
    if (list) el.body.appendChild(list);

    if (mode === 'save') {
      var line = mk('div', 'nameline');
      var input = mk('input');
      input.type = 'text';
      input.id = 'cloud-save-name';
      input.maxLength = 60;
      input.value = (pendingDoc && pendingDoc.name) || 'scene';
      line.appendChild(mk('span', null, T('viz_cloud_name_label', '名称')));
      line.appendChild(input);
      el.body.appendChild(line);
      el.body.appendChild(mk('div', 'hint', T('viz_cloud_save_overwrite_hint',
        '选中上面某一项会覆盖它；不选就是新建一份。')));
    }

    el.btns.appendChild(mk('span', 'spacer'));
    addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
    if (mode === 'save') {
      addBtn(el.btns, T('viz_cloud_save', '保存'), 'go', doSaveFromList);
    } else {
      addBtn(el.btns, T('viz_cloud_open', '打开'), 'go', function () {
        if (selectedId) openFile(selectedId);
      }, !selectedId);
    }
    if (listing) {
      
      [].slice.call(el.btns.querySelectorAll('button.go')).forEach(function (b) {
        b.disabled = true;
      });
    }
  }

  function renderRow(f) {
    var row = mk('div', 'row' + (selectedId === f.id ? ' on' : ''));
    var nm = mk('span', 'nm', f.name);
    if (currentId === f.id) nm.appendChild(mk('span', 'cur', T('viz_cloud_current', '（当前）')));
    row.appendChild(nm);
    row.appendChild(mk('span', 'meta sz', sizeText(f.size)));
    




    row.appendChild(mk('span', 'meta tm', timeText(f.created)));
    row.appendChild(mk('span', 'meta tm', timeText(f.updated)));

    var ops = mk('div', confirmDelId === f.id ? 'ops confirm' : 'ops');
    if (confirmDelId === f.id) {
      
      ops.appendChild(mk('span', 'meta', T('viz_cloud_del_confirm', '确定删除？')));
      addBtn(ops, T('viz_cloud_del_yes', '删除'), 'del', function (ev) {
        ev.stopPropagation();
        confirmDelId = null;
        if (VizChannel.cloud) {
          
          VizChannel.cloud.delete_file(JSON.stringify({ req_id: 0, id: f.id, name: f.name }));
        }
      });
      addBtn(ops, T('viz_cloud_cancel', '取消'), '', function (ev) {
        ev.stopPropagation();
        confirmDelId = null;
        render();
      });
    } else {
      addBtn(ops, T('viz_cloud_rename', '重命名'), '', function (ev) {
        ev.stopPropagation();
        startRename(row, nm, f);
      });
      addBtn(ops, T('viz_cloud_delete', '删除'), 'del', function (ev) {
        ev.stopPropagation();
        confirmDelId = f.id;
        render();
      });
    }
    row.appendChild(ops);

    row.addEventListener('click', function () {
      selectedId = f.id;
      var input = document.getElementById('cloud-save-name');
      if (mode === 'save' && input) input.value = f.name;   
      render();
    });
    row.addEventListener('dblclick', function () {
      if (mode === 'open') openFile(f.id);
    });
    return row;
  }

  function startRename(row, nmEl, f) {
    var input = mk('input');
    input.type = 'text';
    input.value = f.name;
    input.maxLength = 60;
    input.style.cssText = 'flex:1 1 auto;background:#23272c;color:#c5c8c6;border:1px solid #4da3ff;' +
                          'border-radius:4px;padding:2px 6px;font-size:12px;';
    row.replaceChild(input, nmEl);
    input.focus();
    input.select();
    var done = function (commit) {
      var n = (input.value || '').trim();
      if (commit && n && n !== f.name && VizChannel.cloud) {
        VizChannel.cloud.rename_file(JSON.stringify({ req_id: 0, id: f.id, name: n }));
      } else {
        render();
      }
    };
    input.addEventListener('click', function (ev) { ev.stopPropagation(); });
    input.addEventListener('keydown', function (ev) {
      ev.stopPropagation();
      if (ev.key === 'Enter') { ev.preventDefault(); done(true); }
      else if (ev.key === 'Escape') { ev.preventDefault(); done(false); }
    });
    input.addEventListener('blur', function () { done(true); });
  }

  function doSaveFromList() {
    var input = document.getElementById('cloud-save-name');
    var n = input ? (input.value || '').trim() : '';
    if (!n) { if (input) input.focus(); return; }
    var chosen = null;
    files.forEach(function (f) { if (f.id === selectedId) chosen = f; });
    if (!VizChannel.cloud) { close(); return; }
    var doc = pendingDoc;
    toBusy('upload', n);
    
    
    VizChannel.cloud.save_scene(JSON.stringify({
      doc: doc, name: n,
      as_new: !chosen, force: !!chosen, id: chosen ? chosen.id : 0
    }));
  }

  

  function toBusy(phase, name) {
    busy = { phase: phase, name: name, done: 0, total: 0 };
    view = 'busy';
    render();
  }

  function openFile(id) {
    if (!opts || !opts.canOpen || !opts.canOpen()) return;
    var f = null;
    files.forEach(function (x) { if (x.id === id) f = x; });
    if (!f || !VizChannel.cloud) return;
    
    
    toBusy('download', f.name);
    busy.total = f.size || 0;      
    VizChannel.cloud.open_file(JSON.stringify({
      req_id: opts.openReq(), id: id, name: f.name, updated: f.updated,
      has_content: opts.hasContent()
    }));
  }

  



  function refreshList() {
    if (!VizChannel.cloud) return;
    listing = true;
    auth.error = null;      
    render();
    VizChannel.cloud.list_files(JSON.stringify({ req_id: 0 }));
  }

  

  window.CloudUI = {
    

    init: function (o) {
      opts = o;
      build();
    },

    
    ready: function () {
      if (VizChannel.cloud) VizChannel.cloud.auth_state('');
    },

    available: function () { return !!VizChannel.cloud; },
    loggedIn: function () { return auth.state === 'logged_in'; },

    




    refresh: function () { render(); },      

    
    openPicker: function (m, doc) {
      if (!VizChannel.cloud) return;
      mode = m === 'save' ? 'save' : 'open';
      pendingDoc = doc || null;
      selectedId = null;
      confirmDelId = null;
      savedNote = '';
      startingLogin = false;      
      if (auth.state !== 'logged_in') { view = 'auth'; show(); return; }
      view = 'list';
      auth.error = null;
      show();
      refreshList();
    },

    

    showAuth: function (starting) {
      if (!VizChannel.cloud) return;
      startingLogin = !!starting;
      view = 'auth';
      show();
    },

    
    promptSaveName: function (defaultName, doc) {
      if (!VizChannel.cloud) return;
      pendingDoc = doc || null;
      if (pendingDoc && defaultName) pendingDoc.name = defaultName;
      view = 'name';
      show();
    },

    

    onAuth: function (p) {
      var was = auth.state;
      auth = p || { state: 'logged_out' };
      
      
      startingLogin = false;
      if (!isOpen()) return;
      
      if (view === 'auth' && auth.state === 'logged_in' && was !== 'logged_in') {
        view = 'list';
        render();
        refreshList();
        return;
      }
      render();
    },

    onList: function (p) {
      listing = false;
      if (!p.ok) {
        auth.error = p.error || '';
        files = [];
      } else {
        auth.error = null;
        files = p.files || [];
        quota = p.quota || 0;
        used = p.used || 0;
        currentId = p.current_id || null;
      }
      render();
    },

    onSaved: function (p) {
      if (p.ok) {
        currentId = p.id;
        used = p.used || used;
        quota = p.quota || quota;
        


        if (isOpen() && view === 'busy') {
          view = 'list';
          mode = 'open';
          selectedId = p.id;
          savedNote = p.name || '';
          auth.error = null;
          render();
          refreshList();
        }
        return;
      }
      if (p.conflict) { askConflict(p); return; }
      
      
      if (isOpen() && view === 'busy') close();
    },

    onDone: function (p) {
      if (!p.ok && p.error) auth.error = p.error;
      if (isOpen()) refreshList();
    },

    onPrompt: function (p) {
      



      if (p.action === 'show_auth') {
        window.CloudUI.showAuth(p.starting);
        return;
      }
      if (p.action !== 'save_new') return;
      var doc = opts && opts.getDoc ? opts.getDoc() : null;
      window.CloudUI.promptSaveName(p.default_name, doc);
    },

    


    onProgress: function (p) {
      if (!isOpen()) return;
      if (p.phase === 'done') {
        


        if (view === 'busy' && busy && busy.phase === 'download') close();
        return;
      }
      if (view !== 'busy') return;
      busy = p;
      render();
    },

    



    __testShowList: function () {
      mode = 'open';
      view = 'list';
      files = [];
      auth.error = null;
      listing = true;
      show();
    },

    close: function () { close(); }
  };

  

  function askConflict(p) {
    conflict = p.conflict;
    pendingDoc = opts && opts.getDoc ? opts.getDoc() : null;
    view = 'conflict';
    show();
  }

  function renderConflict() {
    el.title.textContent = T('viz_cloud_conflict_title', '云端的版本更新了');
    el.body.appendChild(mk('div', 'msg', T('viz_cloud_conflict_msg',
      '这个场景在别的地方改过了（{time}）。\n继续保存会覆盖掉那边的改动。')
      .replace('{time}', timeText(conflict && conflict.updated))));
    el.btns.appendChild(mk('span', 'spacer'));
    addBtn(el.btns, T('viz_cloud_cancel', '取消'), '', close);
    addBtn(el.btns, T('viz_cloud_conflict_copy', '另存为副本'), '', function () {
      if (VizChannel.cloud) {
        VizChannel.cloud.save_scene(JSON.stringify({
          doc: pendingDoc,
          name: ((conflict && conflict.name) || 'scene') + T('viz_cloud_copy_suffix', ' 副本'),
          as_new: true
        }));
      }
      close();
    });
    addBtn(el.btns, T('viz_cloud_conflict_force', '用我的覆盖'), 'go', function () {
      if (VizChannel.cloud) {
        VizChannel.cloud.save_scene(JSON.stringify({
          doc: pendingDoc, name: conflict && conflict.name, force: true
        }));
      }
      close();
    });
  }
})();
