diff --git a/wails/assets/assets/index-BDrFF8AO.css b/wails/assets/assets/index-BDrFF8AO.css deleted file mode 100644 index 4a66165..0000000 --- a/wails/assets/assets/index-BDrFF8AO.css +++ /dev/null @@ -1 +0,0 @@ -.video-tab[data-v-54d3cc11]{max-width:900px;margin:0 auto}h2[data-v-54d3cc11]{margin-bottom:20px;color:#333}.form-group[data-v-54d3cc11]{margin-bottom:20px}label[data-v-54d3cc11]{display:block;margin-bottom:8px;font-weight:500;color:#555}.input-group[data-v-54d3cc11]{display:flex;gap:10px}.input-group input[data-v-54d3cc11]{flex:1;padding:8px 12px;border:1px solid #ddd;border-radius:4px}.input-group button[data-v-54d3cc11]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.input-group button[data-v-54d3cc11]:hover{background:#5a5080}.radio-group[data-v-54d3cc11]{display:flex;flex-direction:column;gap:10px}.radio-group label[data-v-54d3cc11]{display:flex;align-items:center;gap:8px;font-weight:400}input[type=number][data-v-54d3cc11]{width:100px;padding:8px;border:1px solid #ddd;border-radius:4px}.hint[data-v-54d3cc11]{margin-left:10px;color:#888;font-size:14px}.btn-primary[data-v-54d3cc11]{padding:12px 24px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-primary[data-v-54d3cc11]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-54d3cc11]:disabled{background:#ccc;cursor:not-allowed}.folder-list[data-v-54d3cc11]{border:1px solid #ddd;border-radius:4px;padding:10px;max-height:200px;overflow-y:auto}.folder-item[data-v-54d3cc11]{display:flex;justify-content:space-between;padding:8px;border-bottom:1px solid #eee}.folder-item[data-v-54d3cc11]:last-child{border-bottom:none}.video-count[data-v-54d3cc11]{color:#888;font-size:14px}.results[data-v-54d3cc11]{margin-top:30px}.results h3[data-v-54d3cc11]{margin-bottom:15px}table[data-v-54d3cc11]{width:100%;border-collapse:collapse;background:#fff;border-radius:4px;overflow:hidden}thead[data-v-54d3cc11]{background:#7163ba;color:#fff}th[data-v-54d3cc11],td[data-v-54d3cc11]{padding:12px;text-align:left;border-bottom:1px solid #eee}.success[data-v-54d3cc11]{color:#28a745}.error[data-v-54d3cc11]{color:#dc3545}.progress-bar[data-v-54d3cc11]{position:relative;width:100px;height:20px;background:#eee;border-radius:10px;overflow:hidden}.progress-fill[data-v-54d3cc11]{position:absolute;top:0;left:0;height:100%;background:#28a745;transition:width .3s}.progress-text[data-v-54d3cc11]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:12px;z-index:1}.extract-tab[data-v-efad5166]{max-width:900px;margin:0 auto}h2[data-v-efad5166]{margin-bottom:20px;color:#333}.form-group[data-v-efad5166]{margin-bottom:20px}label[data-v-efad5166]{display:block;margin-bottom:8px;font-weight:500;color:#555}.input-group[data-v-efad5166]{display:flex;gap:10px}.input-group input[data-v-efad5166]{flex:1;padding:8px 12px;border:1px solid #ddd;border-radius:4px}.input-group button[data-v-efad5166]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.input-group button[data-v-efad5166]:hover{background:#5a5080}input[type=number][data-v-efad5166]{width:100px;padding:8px;border:1px solid #ddd;border-radius:4px}.hint[data-v-efad5166]{margin-top:5px;color:#888;font-size:14px}.btn-primary[data-v-efad5166]{padding:12px 24px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-primary[data-v-efad5166]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-efad5166]:disabled{background:#ccc;cursor:not-allowed}.btn-secondary[data-v-efad5166]{padding:12px 24px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-secondary[data-v-efad5166]:hover:not(:disabled){background:#5a6268}.btn-secondary[data-v-efad5166]:disabled{background:#ccc;cursor:not-allowed}.video-list[data-v-efad5166]{border:1px solid #ddd;border-radius:4px;padding:10px;max-height:200px;overflow-y:auto}.video-item[data-v-efad5166]{padding:8px;border-bottom:1px solid #eee}.video-item[data-v-efad5166]:last-child{border-bottom:none}.help-info[data-v-efad5166]{margin-top:20px;padding:15px;background:#f8f9fa;border-radius:4px;white-space:pre-line;line-height:1.6}.results[data-v-efad5166]{margin-top:30px}.results h3[data-v-efad5166]{margin-bottom:15px}.result-summary[data-v-efad5166]{padding:15px;background:#fff;border-radius:4px;border:1px solid #ddd}.result-summary p[data-v-efad5166]{margin:5px 0}.dialog-overlay[data-v-123e9c29]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog[data-v-123e9c29]{background:#fff;border-radius:8px;width:400px;max-width:90vw;box-shadow:0 4px 20px #0000004d}.dialog-header[data-v-123e9c29]{display:flex;justify-content:space-between;align-items:center;padding:20px;border-bottom:1px solid #eee}.dialog-header h3[data-v-123e9c29]{margin:0;color:#333}.close-btn[data-v-123e9c29]{background:none;border:none;font-size:24px;cursor:pointer;color:#999;padding:0;width:30px;height:30px;line-height:30px}.close-btn[data-v-123e9c29]:hover{color:#333}.dialog-body[data-v-123e9c29]{padding:20px}.form-group[data-v-123e9c29]{margin-bottom:15px}.form-group label[data-v-123e9c29]{display:block;margin-bottom:5px;color:#555;font-weight:500}.form-group input[data-v-123e9c29]{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px}.form-group input[data-v-123e9c29]:focus{outline:none;border-color:#7163ba}.error-message[data-v-123e9c29]{color:#dc3545;font-size:14px;margin-top:10px}.dialog-footer[data-v-123e9c29]{display:flex;justify-content:flex-end;gap:10px;padding:20px;border-top:1px solid #eee}.btn-primary[data-v-123e9c29]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.btn-primary[data-v-123e9c29]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-123e9c29]:disabled{background:#ccc;cursor:not-allowed}.btn-secondary[data-v-123e9c29]{padding:8px 16px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer}.btn-secondary[data-v-123e9c29]:hover{background:#5a6268}.app-container[data-v-75b1df04]{display:flex;width:100vw;height:100vh;background:linear-gradient(to bottom,#fefefe,#ededef)}.sidebar[data-v-75b1df04]{width:70px;background:#7163ba;display:flex;flex-direction:column;justify-content:space-between;padding:10px 0;border-right:2px solid #ebedf3}.menu-items[data-v-75b1df04]{display:flex;flex-direction:column;gap:5px}.menu-item[data-v-75b1df04]{width:50px;height:50px;margin:0 auto;background:transparent;border:2px solid #000;border-radius:5px;color:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:12px;transition:all .2s}.menu-item[data-v-75b1df04]:hover{background:#5a5080}.menu-item.active[data-v-75b1df04]{background:#8b4513}.menu-item .icon[data-v-75b1df04]{font-size:18px}.separator[data-v-75b1df04]{height:1px;background:#ffffff4d;margin:5px 10px}.menu-bottom[data-v-75b1df04]{display:flex;flex-direction:column}.main-content[data-v-75b1df04]{flex:1;padding:20px;overflow:auto}.login-overlay[data-v-75b1df04]{position:fixed;top:0;left:0;right:0;bottom:0;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:999}.login-message[data-v-75b1df04]{background:#fff;padding:40px;border-radius:8px;text-align:center;box-shadow:0 4px 20px #0000004d}.login-message h2[data-v-75b1df04]{margin:0 0 10px;color:#333;font-size:24px}.login-message p[data-v-75b1df04]{margin:0;color:#666;font-size:16px}.main-interface[data-v-75b1df04]{display:flex;width:100%;height:100%} diff --git a/wails/assets/assets/index-BaD48VVT.css b/wails/assets/assets/index-BaD48VVT.css new file mode 100644 index 0000000..f6888bc --- /dev/null +++ b/wails/assets/assets/index-BaD48VVT.css @@ -0,0 +1 @@ +.video-tab[data-v-976c97bb]{max-width:900px;margin:0 auto}h2[data-v-976c97bb]{margin-bottom:20px;color:#333}.form-group[data-v-976c97bb]{margin-bottom:20px}label[data-v-976c97bb]{display:block;margin-bottom:8px;font-weight:500;color:#555}.input-group[data-v-976c97bb]{display:flex;gap:10px}.input-group input[data-v-976c97bb]{flex:1;padding:8px 12px;border:1px solid #ddd;border-radius:4px}.input-group button[data-v-976c97bb]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.input-group button[data-v-976c97bb]:hover{background:#5a5080}.radio-group[data-v-976c97bb]{display:flex;flex-direction:column;gap:10px}.radio-group label[data-v-976c97bb]{display:flex;align-items:center;gap:8px;font-weight:400}input[type=number][data-v-976c97bb]{width:100px;padding:8px;border:1px solid #ddd;border-radius:4px}.hint[data-v-976c97bb]{margin-left:10px;color:#888;font-size:14px}.btn-primary[data-v-976c97bb]{padding:12px 24px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-primary[data-v-976c97bb]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-976c97bb]:disabled{background:#ccc;cursor:not-allowed}.folder-list[data-v-976c97bb]{border:1px solid #ddd;border-radius:4px;padding:10px;max-height:200px;overflow-y:auto}.folder-item[data-v-976c97bb]{display:flex;justify-content:space-between;padding:8px;border-bottom:1px solid #eee}.folder-item[data-v-976c97bb]:last-child{border-bottom:none}.video-count[data-v-976c97bb]{color:#888;font-size:14px}.results[data-v-976c97bb]{margin-top:30px}.results h3[data-v-976c97bb]{margin-bottom:15px}table[data-v-976c97bb]{width:100%;border-collapse:collapse;background:#fff;border-radius:4px;overflow:hidden}thead[data-v-976c97bb]{background:#7163ba;color:#fff}th[data-v-976c97bb],td[data-v-976c97bb]{padding:12px;text-align:left;border-bottom:1px solid #eee}.success[data-v-976c97bb]{color:#28a745}.error[data-v-976c97bb]{color:#dc3545}.progress-bar[data-v-976c97bb]{position:relative;width:100px;height:20px;background:#eee;border-radius:10px;overflow:hidden}.progress-fill[data-v-976c97bb]{position:absolute;top:0;left:0;height:100%;background:#28a745;transition:width .3s}.progress-text[data-v-976c97bb]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:12px;z-index:1}.extract-tab[data-v-32ae0b7c]{max-width:900px;margin:0 auto}h2[data-v-32ae0b7c]{margin-bottom:20px;color:#333}.form-group[data-v-32ae0b7c]{margin-bottom:20px}label[data-v-32ae0b7c]{display:block;margin-bottom:8px;font-weight:500;color:#555}.input-group[data-v-32ae0b7c]{display:flex;gap:10px}.input-group input[data-v-32ae0b7c]{flex:1;padding:8px 12px;border:1px solid #ddd;border-radius:4px}.input-group button[data-v-32ae0b7c]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.input-group button[data-v-32ae0b7c]:hover{background:#5a5080}input[type=number][data-v-32ae0b7c]{width:100px;padding:8px;border:1px solid #ddd;border-radius:4px}.hint[data-v-32ae0b7c]{margin-top:5px;color:#888;font-size:14px}.btn-primary[data-v-32ae0b7c]{padding:12px 24px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-primary[data-v-32ae0b7c]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-32ae0b7c]:disabled{background:#ccc;cursor:not-allowed}.btn-secondary[data-v-32ae0b7c]{padding:12px 24px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}.btn-secondary[data-v-32ae0b7c]:hover:not(:disabled){background:#5a6268}.btn-secondary[data-v-32ae0b7c]:disabled{background:#ccc;cursor:not-allowed}.video-list[data-v-32ae0b7c]{border:1px solid #ddd;border-radius:4px;padding:10px;max-height:200px;overflow-y:auto}.video-item[data-v-32ae0b7c]{padding:8px;border-bottom:1px solid #eee}.video-item[data-v-32ae0b7c]:last-child{border-bottom:none}.help-info[data-v-32ae0b7c]{margin-top:20px;padding:15px;background:#f8f9fa;border-radius:4px;white-space:pre-line;line-height:1.6}.results[data-v-32ae0b7c]{margin-top:30px}.results h3[data-v-32ae0b7c]{margin-bottom:15px}.result-summary[data-v-32ae0b7c]{padding:15px;background:#fff;border-radius:4px;border:1px solid #ddd}.result-summary p[data-v-32ae0b7c]{margin:5px 0}.dialog-overlay[data-v-f054741d]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog[data-v-f054741d]{background:#fff;border-radius:8px;width:400px;max-width:90vw;box-shadow:0 4px 20px #0000004d}.dialog-header[data-v-f054741d]{display:flex;justify-content:space-between;align-items:center;padding:20px;border-bottom:1px solid #eee}.dialog-header h3[data-v-f054741d]{margin:0;color:#333}.close-btn[data-v-f054741d]{background:none;border:none;font-size:24px;cursor:pointer;color:#999;padding:0;width:30px;height:30px;line-height:30px}.close-btn[data-v-f054741d]:hover{color:#333}.dialog-body[data-v-f054741d]{padding:20px}.form-group[data-v-f054741d]{margin-bottom:15px}.form-group label[data-v-f054741d]{display:block;margin-bottom:5px;color:#555;font-weight:500}.form-group input[data-v-f054741d]{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px}.form-group input[data-v-f054741d]:focus{outline:none;border-color:#7163ba}.error-message[data-v-f054741d]{color:#dc3545;font-size:14px;margin-top:10px}.dialog-footer[data-v-f054741d]{display:flex;justify-content:flex-end;gap:10px;padding:20px;border-top:1px solid #eee}.btn-primary[data-v-f054741d]{padding:8px 16px;background:#7163ba;color:#fff;border:none;border-radius:4px;cursor:pointer}.btn-primary[data-v-f054741d]:hover:not(:disabled){background:#5a5080}.btn-primary[data-v-f054741d]:disabled{background:#ccc;cursor:not-allowed}.btn-secondary[data-v-f054741d]{padding:8px 16px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer}.btn-secondary[data-v-f054741d]:hover{background:#5a6268}.app-container[data-v-8a8ce913]{display:flex;width:100vw;height:100vh;background:linear-gradient(to bottom,#fefefe,#ededef)}.sidebar[data-v-8a8ce913]{width:70px;background:#7163ba;display:flex;flex-direction:column;justify-content:space-between;padding:10px 0;border-right:2px solid #ebedf3}.menu-items[data-v-8a8ce913]{display:flex;flex-direction:column;gap:5px}.menu-item[data-v-8a8ce913]{width:50px;height:50px;margin:0 auto;background:transparent;border:2px solid #000;border-radius:5px;color:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:12px;transition:all .2s}.menu-item[data-v-8a8ce913]:hover{background:#5a5080}.menu-item.active[data-v-8a8ce913]{background:#8b4513}.menu-item .icon[data-v-8a8ce913]{font-size:18px}.separator[data-v-8a8ce913]{height:1px;background:#ffffff4d;margin:5px 10px}.menu-bottom[data-v-8a8ce913]{display:flex;flex-direction:column}.main-content[data-v-8a8ce913]{flex:1;padding:20px;overflow:auto}.login-overlay[data-v-8a8ce913]{position:fixed;top:0;left:0;right:0;bottom:0;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:999}.login-message[data-v-8a8ce913]{background:#fff;padding:40px;border-radius:8px;text-align:center;box-shadow:0 4px 20px #0000004d}.login-message h2[data-v-8a8ce913]{margin:0 0 10px;color:#333;font-size:24px}.login-message p[data-v-8a8ce913]{margin:0;color:#666;font-size:16px}.main-interface[data-v-8a8ce913]{display:flex;width:100%;height:100%} diff --git a/wails/assets/assets/index-DSuQhuGl.js b/wails/assets/assets/index-IwiMqFON.js similarity index 99% rename from wails/assets/assets/index-DSuQhuGl.js rename to wails/assets/assets/index-IwiMqFON.js index 04a86c0..e1ced2f 100644 --- a/wails/assets/assets/index-DSuQhuGl.js +++ b/wails/assets/assets/index-IwiMqFON.js @@ -14,4 +14,4 @@ import{Create as _e,Call as vt}from"@wailsio/runtime";(function(){const t=docume * @vue/runtime-dom v3.5.26 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let qs;const jn=typeof window<"u"&&window.trustedTypes;if(jn)try{qs=jn.createPolicy("vue",{createHTML:e=>e})}catch{}const si=qs?e=>qs.createHTML(e):e=>e,il="http://www.w3.org/2000/svg",ol="http://www.w3.org/1998/Math/MathML",Re=typeof document<"u"?document:null,Hn=Re&&Re.createElement("template"),ll={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const r=t==="svg"?Re.createElementNS(il,e):t==="mathml"?Re.createElementNS(ol,e):s?Re.createElement(e,{is:s}):Re.createElement(e);return e==="select"&&n&&n.multiple!=null&&r.setAttribute("multiple",n.multiple),r},createText:e=>Re.createTextNode(e),createComment:e=>Re.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Re.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,r,i){const o=s?s.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),s),!(r===i||!(r=r.nextSibling)););else{Hn.innerHTML=si(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const l=Hn.content;if(n==="svg"||n==="mathml"){const u=l.firstChild;for(;u.firstChild;)l.appendChild(u.firstChild);l.removeChild(u)}t.insertBefore(l,s)}return[o?o.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},cl=Symbol("_vtc");function ul(e,t,s){const n=e[cl];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ls=Symbol("_vod"),ni=Symbol("_vsh"),fl={name:"show",beforeMount(e,{value:t},{transition:s}){e[ls]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):Ct(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:n}){!t!=!s&&(n?t?(n.beforeEnter(e),Ct(e,!0),n.enter(e)):n.leave(e,()=>{Ct(e,!1)}):Ct(e,t))},beforeUnmount(e,{value:t}){Ct(e,t)}};function Ct(e,t){e.style.display=t?e[ls]:"none",e[ni]=!t}const al=Symbol(""),dl=/(?:^|;)\s*display\s*:/;function hl(e,t,s){const n=e.style,r=X(s);let i=!1;if(s&&!r){if(t)if(X(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();s[l]==null&&Qt(n,l,"")}else for(const o in t)s[o]==null&&Qt(n,o,"");for(const o in s)o==="display"&&(i=!0),Qt(n,o,s[o])}else if(r){if(t!==s){const o=n[al];o&&(s+=";"+o),n.cssText=s,i=dl.test(s)}}else t&&e.removeAttribute("style");ls in e&&(e[ls]=i?n.display:"",e[ni]&&(n.display="none"))}const Vn=/\s*!important$/;function Qt(e,t,s){if(R(s))s.forEach(n=>Qt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=pl(e,t);Vn.test(s)?e.setProperty(it(n),s.replace(Vn,""),"important"):e[n]=s}}const Un=["Webkit","Moz","ms"],Fs={};function pl(e,t){const s=Fs[t];if(s)return s;let n=Je(t);if(n!=="filter"&&n in e)return Fs[t]=n;n=nr(n);for(let r=0;rRs||(_l.then(()=>Rs=0),Rs=Date.now());function yl(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;Me(xl(n,s.value),t,5,[n])};return s.value=e,s.attached=bl(),s}function xl(e,t){if(R(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>r=>!r._stopped&&n&&n(r))}else return t}const Jn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Sl=(e,t,s,n,r,i)=>{const o=r==="svg";t==="class"?ul(e,n,o):t==="style"?hl(e,s,n):us(t)?Gs(t)||ml(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):wl(e,t,n,o))?(Wn(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Bn(e,t,n,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!X(n))?Wn(e,Je(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Bn(e,t,n,o))};function wl(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Jn(t)&&D(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Jn(t)&&X(s)?!1:t in e}const cs=e=>{const t=e.props["onUpdate:modelValue"]||!1;return R(t)?s=>Yt(t,s):t};function Cl(e){e.target.composing=!0}function Gn(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ht=Symbol("_assign");function Yn(e,t,s){return t&&(e=e.trim()),s&&(e=Xs(e)),e}const rt={created(e,{modifiers:{lazy:t,trim:s,number:n}},r){e[ht]=cs(r);const i=n||r.props&&r.props.type==="number";tt(e,t?"change":"input",o=>{o.target.composing||e[ht](Yn(e.value,s,i))}),(s||i)&&tt(e,"change",()=>{e.value=Yn(e.value,s,i)}),t||(tt(e,"compositionstart",Cl),tt(e,"compositionend",Gn),tt(e,"change",Gn))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:r,number:i}},o){if(e[ht]=cs(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Xs(e.value):e.value,u=t??"";l!==u&&(document.activeElement===e&&e.type!=="range"&&(n&&t===s||r&&e.value.trim()===u)||(e.value=u))}},zn={created(e,{value:t},s){e.checked=es(t,s.props.value),e[ht]=cs(s),tt(e,"change",()=>{e[ht](Tl(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[ht]=cs(n),t!==s&&(e.checked=es(t,n.props.value))}};function Tl(e){return"_value"in e?e._value:e.value}const El=["ctrl","shift","alt","meta"],Ol={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>El.some(s=>e[`${s}Key`]&&!t.includes(s))},Pl=(e,t)=>{const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=(r,...i)=>{for(let o=0;o{const t=Il().createApp(...e),{mount:s}=t;return t.mount=n=>{const r=Rl(n);if(!r)return;const i=t._component;!D(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=s(r,!1,Fl(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t};function Fl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Rl(e){return X(e)?document.querySelector(e):e}class dn{constructor(t={}){"videoPath"in t||(this.videoPath=""),"outputPath"in t||(this.outputPath=""),"success"in t||(this.success=!1),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new dn(s)}}class bs{constructor(t={}){"path"in t||(this.path=""),"name"in t||(this.name=""),"videoCount"in t||(this.videoCount=0),"videoPaths"in t||(this.videoPaths=[]),Object.assign(this,t)}static createFrom(t={}){const s=$l;let n=typeof t=="string"?JSON.parse(t):t;return"videoPaths"in n&&(n.videoPaths=s(n.videoPaths)),new bs(n)}}class hn{constructor(t={}){"Code"in t||(this.Code=0),"Msg"in t||(this.Msg=""),"Data"in t||(this.Data=null),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new hn(s)}}class pn{constructor(t={}){"index"in t||(this.index=0),"fileName"in t||(this.fileName=""),"filePath"in t||(this.filePath=""),"size"in t||(this.size=""),"seconds"in t||(this.seconds=0),"status"in t||(this.status=""),"progress"in t||(this.progress=""),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new pn(s)}}const $l=_e.Array(_e.Any),Dl=bs.createFrom;_e.Array(Dl);function Ll(e,t){return vt.ByID(2350837569,e,t).then(s=>jl(s))}const Nl=hn.createFrom,jl=_e.Nullable(Nl);function Hl(e){return vt.ByID(1728131056,e).then(t=>Kl(t))}function Vl(e){return vt.ByID(2398906893,e).then(t=>Bl(t))}const Ul=dn.createFrom,Kl=_e.Array(Ul),Bl=_e.Array(_e.Any);function ri(){return vt.ByID(23551676)}_e.Array(_e.Any);function Wl(e){return vt.ByID(2660164351,e).then(t=>Jl(t))}function kl(e){return vt.ByID(2209109868,e).then(t=>Yl(t))}const ql=pn.createFrom,Jl=_e.Array(ql),Gl=bs.createFrom,Yl=_e.Array(Gl),ys=(e,t)=>{const s=e.__vccOpts||e;for(const[n,r]of t)s[n]=r;return s},zl={class:"video-tab"},Xl={class:"form-group"},Zl={class:"input-group"},Ql={key:0,class:"form-group"},ec={class:"folder-list"},tc={class:"video-count"},sc={class:"form-group"},nc={class:"radio-group"},rc={class:"form-group"},ic=["max","disabled"],oc={class:"hint"},lc={class:"form-group"},cc={class:"input-group"},uc={class:"form-group"},fc=["disabled"],ac={key:1,class:"results"},dc={class:"progress-bar"},hc={class:"progress-text"},pc={__name:"VideoTab",setup(e){const t=Y(""),s=Y([]),n=Y(1),r=Y(1),i=Y(0),o=Y(""),l=Y(!1),u=Y([]),d=ct(()=>t.value&&r.value>0&&r.value<=i.value&&!l.value),a=async()=>{try{const P=await ri();if(P&&P.trim()){t.value=P.trim();try{s.value=await kl(t.value),w()}catch(g){alert("列出文件夹失败: "+g.message)}}}catch(P){console.error("选择文件夹失败:",P),alert("选择文件夹失败: "+(P.message||"未知错误"))}},p=()=>{const P=document.createElement("input");P.type="file",P.accept="image/*",P.onchange=g=>{const E=g.target.files[0];E&&(o.value=E.name)},P.click()},w=()=>{n.value===1?i.value=s.value.reduce((P,g)=>P*g.videoCount,1):i.value=s.value.length>0?s.value[0].videoCount:0},O=async()=>{if(d.value){l.value=!0,u.value=[];try{const P={folderPath:t.value,num:r.value,joinType:n.value,auditImagePath:o.value,folderInfos:s.value};try{const g=await Wl(P);g&&(u.value=g)}catch(g){alert("拼接视频失败: "+g.message)}}catch(P){alert("拼接失败: "+P.message)}finally{l.value=!1}}};return(P,g)=>(J(),Q("div",zl,[g[14]||(g[14]=_("h2",null,"视频拼接",-1)),_("div",Xl,[g[5]||(g[5]=_("label",null,"选择文件夹:",-1)),_("div",Zl,[Pe(_("input",{type:"text","onUpdate:modelValue":g[0]||(g[0]=E=>t.value=E),placeholder:"请选择包含视频文件夹的目录",readonly:""},null,512),[[rt,t.value]]),_("button",{onClick:a},"选择文件夹")])]),s.value.length>0?(J(),Q("div",Ql,[g[6]||(g[6]=_("label",null,"文件夹列表:",-1)),_("div",ec,[(J(!0),Q(de,null,Hs(s.value,(E,$)=>(J(),Q("div",{key:$,class:"folder-item"},[_("span",null,z(E.name),1),_("span",tc,z(E.videoCount)+" 个视频",1)]))),128))])])):ge("",!0),_("div",sc,[g[9]||(g[9]=_("label",null,"拼接模式:",-1)),_("div",nc,[_("label",null,[Pe(_("input",{type:"radio","onUpdate:modelValue":g[1]||(g[1]=E=>n.value=E),value:1},null,512),[[zn,n.value]]),g[7]||(g[7]=Ws(" 组合拼接(从每个文件夹随机选择) ",-1))]),_("label",null,[Pe(_("input",{type:"radio","onUpdate:modelValue":g[2]||(g[2]=E=>n.value=E),value:2},null,512),[[zn,n.value]]),g[8]||(g[8]=Ws(" 顺序拼接(按索引顺序) ",-1))])])]),_("div",rc,[g[10]||(g[10]=_("label",null,"生成数量:",-1)),Pe(_("input",{type:"number","onUpdate:modelValue":g[3]||(g[3]=E=>r.value=E),min:1,max:i.value,disabled:l.value},null,8,ic),[[rt,r.value,void 0,{number:!0}]]),_("span",oc,"最大可生成:"+z(i.value)+" 个",1)]),_("div",lc,[g[11]||(g[11]=_("label",null,"审核图片(可选):",-1)),_("div",cc,[Pe(_("input",{type:"text","onUpdate:modelValue":g[4]||(g[4]=E=>o.value=E),placeholder:"选择审核图片",readonly:""},null,512),[[rt,o.value]]),_("button",{onClick:p},"选择图片")])]),_("div",uc,[_("button",{class:"btn-primary",onClick:O,disabled:!d.value||l.value},z(l.value?"处理中...":"开始拼接"),9,fc)]),u.value.length>0?(J(),Q("div",ac,[g[13]||(g[13]=_("h3",null,"拼接结果",-1)),_("table",null,[g[12]||(g[12]=_("thead",null,[_("tr",null,[_("th",null,"序号"),_("th",null,"文件名"),_("th",null,"大小"),_("th",null,"时长"),_("th",null,"状态"),_("th",null,"进度")])],-1)),_("tbody",null,[(J(!0),Q(de,null,Hs(u.value,E=>(J(),Q("tr",{key:E.index},[_("td",null,z(E.index),1),_("td",null,z(E.fileName),1),_("td",null,z(E.size),1),_("td",null,z(E.seconds)+"秒",1),_("td",{class:pt(E.status==="拼接成功"?"success":"error")},z(E.status),3),_("td",null,[_("div",dc,[_("div",{class:"progress-fill",style:ds({width:E.progress})},null,4),_("span",hc,z(E.progress),1)])])]))),128))])])])):ge("",!0)]))}},gc=ys(pc,[["__scopeId","data-v-54d3cc11"]]),mc={class:"extract-tab"},vc={class:"form-group"},_c={class:"input-group"},bc={key:0,class:"form-group"},yc={class:"video-list"},xc={class:"hint"},Sc={class:"form-group"},wc=["disabled"],Cc={class:"form-group"},Tc=["disabled"],Ec=["disabled"],Oc={key:1,class:"help-info"},Pc={key:2,class:"results"},Ac={class:"result-summary"},Ic={__name:"ExtractTab",setup(e){const t=Y(""),s=Y([]),n=Y(1),r=Y(!1),i=Y(""),o=Y([]),l=ct(()=>t.value&&s.value.length>0&&n.value>0&&!r.value),u=ct(()=>t.value&&s.value.length>0&&!r.value),d=ct(()=>o.value.filter(g=>g.success).length),a=ct(()=>o.value.filter(g=>!g.success).length),p=async()=>{try{const g=await ri();if(g&&g.trim()){t.value=g.trim();try{s.value=await Vl(t.value)}catch(E){alert("列出视频失败: "+E.message)}}}catch(g){console.error("选择文件夹失败:",g),alert("选择文件夹失败: "+(g.message||"未知错误"))}},w=g=>g.split("/").pop()||g.split("\\").pop()||g,O=async()=>{if(l.value){r.value=!0,o.value=[],i.value=`开始处理,每个视频将生成 ${n.value} 个抽帧视频...`;try{const g={folderPath:t.value,extractCount:n.value},E=s.value.length*n.value;i.value=`处理中... (0/${E})`;try{const $=await Hl(g);$&&(o.value=$,i.value=`全部完成! 共处理 ${E} 个任务,成功 ${d.value} 个,失败 ${a.value} 个`)}catch($){alert("抽帧失败: "+$.message),i.value="处理失败"}}catch(g){alert("抽帧失败: "+g.message),i.value="处理失败: "+g.message}finally{r.value=!1}}},P=async()=>{if(u.value){r.value=!0,o.value=[],i.value="开始修改元数据...";try{if(extractService&&extractService.ModifyVideosMetadata){const g=s.value.length;i.value=`处理中... (0/${g})`;const E=await extractService.ModifyVideosMetadata(t.value);E&&(o.value=E,i.value=`全部完成! 共处理 ${g} 个任务,成功 ${d.value} 个,失败 ${a.value} 个`)}}catch(g){alert("修改失败: "+g.message),i.value="处理失败: "+g.message}finally{r.value=!1}}};return(g,E)=>(J(),Q("div",mc,[E[6]||(E[6]=_("h2",null,"视频抽帧",-1)),_("div",vc,[E[2]||(E[2]=_("label",null,"选择文件夹:",-1)),_("div",_c,[Pe(_("input",{type:"text","onUpdate:modelValue":E[0]||(E[0]=$=>t.value=$),placeholder:"请选择包含视频文件的目录",readonly:""},null,512),[[rt,t.value]]),_("button",{onClick:p},"选择文件夹")])]),s.value.length>0?(J(),Q("div",bc,[E[3]||(E[3]=_("label",null,"视频文件:",-1)),_("div",yc,[(J(!0),Q(de,null,Hs(s.value,($,V)=>(J(),Q("div",{key:V,class:"video-item"},z(w($)),1))),128))]),_("p",xc,"共 "+z(s.value.length)+" 个视频文件",1)])):ge("",!0),_("div",Sc,[E[4]||(E[4]=_("label",null,"每个视频生成数量:",-1)),Pe(_("input",{type:"number","onUpdate:modelValue":E[1]||(E[1]=$=>n.value=$),min:1,disabled:r.value},null,8,wc),[[rt,n.value,void 0,{number:!0}]])]),_("div",Cc,[_("button",{class:"btn-primary",onClick:O,disabled:!l.value||r.value},z(r.value?"处理中...":"开始抽帧"),9,Tc),_("button",{class:"btn-secondary",onClick:P,disabled:!u.value||r.value,style:{"margin-left":"10px"}},z(r.value?"处理中...":"开始修改元数据"),9,Ec)]),i.value?(J(),Q("div",Oc,z(i.value),1)):ge("",!0),o.value.length>0?(J(),Q("div",Pc,[E[5]||(E[5]=_("h3",null,"处理结果",-1)),_("div",Ac,[_("p",null,"成功: "+z(d.value)+" 个",1),_("p",null,"失败: "+z(a.value)+" 个",1)])])):ge("",!0)]))}},Mc=ys(Ic,[["__scopeId","data-v-efad5166"]]),Fc={class:"dialog-header"},Rc={class:"dialog-body"},$c={class:"form-group"},Dc={class:"form-group"},Lc={key:0,class:"error-message"},Nc={class:"dialog-footer"},jc=["disabled"],Hc={__name:"LoginDialog",props:{allowClose:{type:Boolean,default:!1}},emits:["close","login-success"],setup(e,{emit:t}){const s=e,n=t,r=Y(""),i=Y(""),o=Y(!1),l=Y(""),u=()=>{s.allowClose&&n("close")},d=async()=>{if(!r.value||!i.value){l.value="请输入用户名和密码";return}o.value=!0,l.value="";try{const a=await Ll(r.value,i.value);a&&a.Code===200?(n("login-success"),n("close")):l.value=a&&a.Msg||"登录失败"}catch(a){l.value="登录失败: "+(a.message||a)}finally{o.value=!1}};return(a,p)=>(J(),Q("div",{class:"dialog-overlay",onClick:u},[_("div",{class:"dialog",onClick:p[4]||(p[4]=Pl(()=>{},["stop"]))},[_("div",Fc,[p[5]||(p[5]=_("h3",null,"用户登录",-1)),e.allowClose?(J(),Q("button",{key:0,class:"close-btn",onClick:p[0]||(p[0]=w=>a.$emit("close"))},"×")):ge("",!0)]),_("div",Rc,[_("div",$c,[p[6]||(p[6]=_("label",null,"用户名:",-1)),Pe(_("input",{type:"text","onUpdate:modelValue":p[1]||(p[1]=w=>r.value=w),placeholder:"请输入用户名"},null,512),[[rt,r.value]])]),_("div",Dc,[p[7]||(p[7]=_("label",null,"密码:",-1)),Pe(_("input",{type:"password","onUpdate:modelValue":p[2]||(p[2]=w=>i.value=w),placeholder:"请输入密码"},null,512),[[rt,i.value]])]),l.value?(J(),Q("div",Lc,z(l.value),1)):ge("",!0)]),_("div",Nc,[e.allowClose?(J(),Q("button",{key:0,class:"btn-secondary",onClick:p[3]||(p[3]=w=>a.$emit("close"))},"取消")):ge("",!0),_("button",{class:"btn-primary",onClick:d,disabled:o.value},z(o.value?"登录中...":"登录"),9,jc)])])]))}},Vc=ys(Hc,[["__scopeId","data-v-123e9c29"]]),Uc={class:"app-container"},Kc={key:0,class:"login-overlay"},Bc={class:"main-interface"},Wc={class:"sidebar"},kc={class:"menu-items"},qc={class:"main-content"},Jc={__name:"App",setup(e){const t=Y("video"),s=Y(!1),n=Y(!1),r=()=>{const u=localStorage.getItem("isLoggedIn"),d=localStorage.getItem("loginTime");if(u==="true"&&d){const a=Date.now(),p=parseInt(d);(a-p)/(1e3*60*60)<24?n.value=!0:(localStorage.removeItem("isLoggedIn"),localStorage.removeItem("loginTime"),n.value=!1,s.value=!0)}else n.value=!1,s.value=!0},i=()=>{n.value=!0,s.value=!1,localStorage.setItem("isLoggedIn","true"),localStorage.setItem("loginTime",Date.now().toString())},o=()=>{n.value&&(s.value=!1)},l=()=>{confirm("确定要退出登录吗?")&&(n.value=!1,s.value=!0,localStorage.removeItem("isLoggedIn"),localStorage.removeItem("loginTime"))};return Rr(()=>{r()}),(u,d)=>(J(),Q("div",Uc,[n.value?ge("",!0):(J(),Q("div",Kc,[...d[2]||(d[2]=[_("div",{class:"login-message"},[_("h2",null,"请先登录"),_("p",null,"您需要登录后才能使用此应用")],-1)])])),Pe(_("div",Bc,[_("div",Wc,[_("div",kc,[_("button",{class:pt(["menu-item",{active:t.value==="video"}]),onClick:d[0]||(d[0]=a=>t.value="video")},[...d[3]||(d[3]=[_("span",{class:"icon"},"🎬",-1),_("span",null,"视频",-1)])],2),d[5]||(d[5]=_("div",{class:"separator"},null,-1)),_("button",{class:pt(["menu-item",{active:t.value==="extract"}]),onClick:d[1]||(d[1]=a=>t.value="extract")},[...d[4]||(d[4]=[_("span",{class:"icon"},"✂️",-1),_("span",null,"抽帧",-1)])],2)]),_("div",{class:"menu-bottom"},[_("button",{class:"menu-item",onClick:l},[...d[6]||(d[6]=[_("span",{class:"icon"},"👤",-1)])])])]),_("div",qc,[t.value==="video"?(J(),Xt(gc,{key:0})):ge("",!0),t.value==="extract"?(J(),Xt(Mc,{key:1})):ge("",!0)])],512),[[fl,n.value]]),s.value?(J(),Xt(Vc,{key:1,"allow-close":n.value,onClose:o,onLoginSuccess:i},null,8,["allow-close"])):ge("",!0)]))}},Gc=ys(Jc,[["__scopeId","data-v-75b1df04"]]);Ml(Gc).mount("#app"); +**/let qs;const jn=typeof window<"u"&&window.trustedTypes;if(jn)try{qs=jn.createPolicy("vue",{createHTML:e=>e})}catch{}const si=qs?e=>qs.createHTML(e):e=>e,il="http://www.w3.org/2000/svg",ol="http://www.w3.org/1998/Math/MathML",Re=typeof document<"u"?document:null,Hn=Re&&Re.createElement("template"),ll={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const r=t==="svg"?Re.createElementNS(il,e):t==="mathml"?Re.createElementNS(ol,e):s?Re.createElement(e,{is:s}):Re.createElement(e);return e==="select"&&n&&n.multiple!=null&&r.setAttribute("multiple",n.multiple),r},createText:e=>Re.createTextNode(e),createComment:e=>Re.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Re.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,r,i){const o=s?s.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),s),!(r===i||!(r=r.nextSibling)););else{Hn.innerHTML=si(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const l=Hn.content;if(n==="svg"||n==="mathml"){const u=l.firstChild;for(;u.firstChild;)l.appendChild(u.firstChild);l.removeChild(u)}t.insertBefore(l,s)}return[o?o.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},cl=Symbol("_vtc");function ul(e,t,s){const n=e[cl];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ls=Symbol("_vod"),ni=Symbol("_vsh"),fl={name:"show",beforeMount(e,{value:t},{transition:s}){e[ls]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):Ct(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:n}){!t!=!s&&(n?t?(n.beforeEnter(e),Ct(e,!0),n.enter(e)):n.leave(e,()=>{Ct(e,!1)}):Ct(e,t))},beforeUnmount(e,{value:t}){Ct(e,t)}};function Ct(e,t){e.style.display=t?e[ls]:"none",e[ni]=!t}const al=Symbol(""),dl=/(?:^|;)\s*display\s*:/;function hl(e,t,s){const n=e.style,r=X(s);let i=!1;if(s&&!r){if(t)if(X(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();s[l]==null&&Qt(n,l,"")}else for(const o in t)s[o]==null&&Qt(n,o,"");for(const o in s)o==="display"&&(i=!0),Qt(n,o,s[o])}else if(r){if(t!==s){const o=n[al];o&&(s+=";"+o),n.cssText=s,i=dl.test(s)}}else t&&e.removeAttribute("style");ls in e&&(e[ls]=i?n.display:"",e[ni]&&(n.display="none"))}const Vn=/\s*!important$/;function Qt(e,t,s){if(R(s))s.forEach(n=>Qt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=pl(e,t);Vn.test(s)?e.setProperty(it(n),s.replace(Vn,""),"important"):e[n]=s}}const Un=["Webkit","Moz","ms"],Fs={};function pl(e,t){const s=Fs[t];if(s)return s;let n=Je(t);if(n!=="filter"&&n in e)return Fs[t]=n;n=nr(n);for(let r=0;rRs||(_l.then(()=>Rs=0),Rs=Date.now());function yl(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;Me(xl(n,s.value),t,5,[n])};return s.value=e,s.attached=bl(),s}function xl(e,t){if(R(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>r=>!r._stopped&&n&&n(r))}else return t}const Jn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Sl=(e,t,s,n,r,i)=>{const o=r==="svg";t==="class"?ul(e,n,o):t==="style"?hl(e,s,n):us(t)?Gs(t)||ml(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):wl(e,t,n,o))?(Wn(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Bn(e,t,n,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!X(n))?Wn(e,Je(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Bn(e,t,n,o))};function wl(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Jn(t)&&D(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Jn(t)&&X(s)?!1:t in e}const cs=e=>{const t=e.props["onUpdate:modelValue"]||!1;return R(t)?s=>Yt(t,s):t};function Cl(e){e.target.composing=!0}function Gn(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ht=Symbol("_assign");function Yn(e,t,s){return t&&(e=e.trim()),s&&(e=Xs(e)),e}const rt={created(e,{modifiers:{lazy:t,trim:s,number:n}},r){e[ht]=cs(r);const i=n||r.props&&r.props.type==="number";tt(e,t?"change":"input",o=>{o.target.composing||e[ht](Yn(e.value,s,i))}),(s||i)&&tt(e,"change",()=>{e.value=Yn(e.value,s,i)}),t||(tt(e,"compositionstart",Cl),tt(e,"compositionend",Gn),tt(e,"change",Gn))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:r,number:i}},o){if(e[ht]=cs(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Xs(e.value):e.value,u=t??"";l!==u&&(document.activeElement===e&&e.type!=="range"&&(n&&t===s||r&&e.value.trim()===u)||(e.value=u))}},zn={created(e,{value:t},s){e.checked=es(t,s.props.value),e[ht]=cs(s),tt(e,"change",()=>{e[ht](Tl(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[ht]=cs(n),t!==s&&(e.checked=es(t,n.props.value))}};function Tl(e){return"_value"in e?e._value:e.value}const El=["ctrl","shift","alt","meta"],Ol={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>El.some(s=>e[`${s}Key`]&&!t.includes(s))},Pl=(e,t)=>{const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=(r,...i)=>{for(let o=0;o{const t=Il().createApp(...e),{mount:s}=t;return t.mount=n=>{const r=Rl(n);if(!r)return;const i=t._component;!D(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=s(r,!1,Fl(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t};function Fl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Rl(e){return X(e)?document.querySelector(e):e}class dn{constructor(t={}){"videoPath"in t||(this.videoPath=""),"outputPath"in t||(this.outputPath=""),"success"in t||(this.success=!1),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new dn(s)}}class bs{constructor(t={}){"path"in t||(this.path=""),"name"in t||(this.name=""),"videoCount"in t||(this.videoCount=0),"videoPaths"in t||(this.videoPaths=[]),Object.assign(this,t)}static createFrom(t={}){const s=$l;let n=typeof t=="string"?JSON.parse(t):t;return"videoPaths"in n&&(n.videoPaths=s(n.videoPaths)),new bs(n)}}class hn{constructor(t={}){"Code"in t||(this.Code=0),"Msg"in t||(this.Msg=""),"Data"in t||(this.Data=null),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new hn(s)}}class pn{constructor(t={}){"index"in t||(this.index=0),"fileName"in t||(this.fileName=""),"filePath"in t||(this.filePath=""),"size"in t||(this.size=""),"seconds"in t||(this.seconds=0),"status"in t||(this.status=""),"progress"in t||(this.progress=""),Object.assign(this,t)}static createFrom(t={}){let s=typeof t=="string"?JSON.parse(t):t;return new pn(s)}}const $l=_e.Array(_e.Any),Dl=bs.createFrom;_e.Array(Dl);function Ll(e,t){return vt.ByID(2350837569,e,t).then(s=>jl(s))}const Nl=hn.createFrom,jl=_e.Nullable(Nl);function Hl(e){return vt.ByID(1728131056,e).then(t=>Kl(t))}function Vl(e){return vt.ByID(2398906893,e).then(t=>Bl(t))}const Ul=dn.createFrom,Kl=_e.Array(Ul),Bl=_e.Array(_e.Any);function ri(){return vt.ByID(23551676)}_e.Array(_e.Any);function Wl(e){return vt.ByID(2660164351,e).then(t=>Jl(t))}function kl(e){return vt.ByID(2209109868,e).then(t=>Yl(t))}const ql=pn.createFrom,Jl=_e.Array(ql),Gl=bs.createFrom,Yl=_e.Array(Gl),ys=(e,t)=>{const s=e.__vccOpts||e;for(const[n,r]of t)s[n]=r;return s},zl={class:"video-tab"},Xl={class:"form-group"},Zl={class:"input-group"},Ql={key:0,class:"form-group"},ec={class:"folder-list"},tc={class:"video-count"},sc={class:"form-group"},nc={class:"radio-group"},rc={class:"form-group"},ic=["max","disabled"],oc={class:"hint"},lc={class:"form-group"},cc={class:"input-group"},uc={class:"form-group"},fc=["disabled"],ac={key:1,class:"results"},dc={class:"progress-bar"},hc={class:"progress-text"},pc={__name:"VideoTab",setup(e){const t=Y(""),s=Y([]),n=Y(1),r=Y(1),i=Y(0),o=Y(""),l=Y(!1),u=Y([]),d=ct(()=>t.value&&r.value>0&&r.value<=i.value&&!l.value),a=async()=>{try{const P=await ri();if(P&&P.trim()){t.value=P.trim();try{s.value=await kl(t.value),w()}catch(g){alert("列出文件夹失败: "+g.message)}}}catch(P){console.error("选择文件夹失败:",P),alert("选择文件夹失败: "+(P.message||"未知错误"))}},p=()=>{const P=document.createElement("input");P.type="file",P.accept="image/*",P.onchange=g=>{const E=g.target.files[0];E&&(o.value=E.name)},P.click()},w=()=>{n.value===1?i.value=s.value.reduce((P,g)=>P*g.videoCount,1):i.value=s.value.length>0?s.value[0].videoCount:0},O=async()=>{if(d.value){l.value=!0,u.value=[];try{const P={folderPath:t.value,num:r.value,joinType:n.value,auditImagePath:o.value,folderInfos:s.value};try{const g=await Wl(P);g&&(u.value=g)}catch(g){alert("拼接视频失败: "+g.message)}}catch(P){alert("拼接失败: "+P.message)}finally{l.value=!1}}};return(P,g)=>(J(),Q("div",zl,[g[14]||(g[14]=_("h2",null,"视频拼接",-1)),_("div",Xl,[g[5]||(g[5]=_("label",null,"选择文件夹:",-1)),_("div",Zl,[Pe(_("input",{type:"text","onUpdate:modelValue":g[0]||(g[0]=E=>t.value=E),placeholder:"请选择包含视频文件夹的目录",readonly:""},null,512),[[rt,t.value]]),_("button",{onClick:a},"选择文件夹")])]),s.value.length>0?(J(),Q("div",Ql,[g[6]||(g[6]=_("label",null,"文件夹列表:",-1)),_("div",ec,[(J(!0),Q(de,null,Hs(s.value,(E,$)=>(J(),Q("div",{key:$,class:"folder-item"},[_("span",null,z(E.name),1),_("span",tc,z(E.videoCount)+" 个视频",1)]))),128))])])):ge("",!0),_("div",sc,[g[9]||(g[9]=_("label",null,"拼接模式:",-1)),_("div",nc,[_("label",null,[Pe(_("input",{type:"radio","onUpdate:modelValue":g[1]||(g[1]=E=>n.value=E),value:1},null,512),[[zn,n.value]]),g[7]||(g[7]=Ws(" 组合拼接(从每个文件夹随机选择) ",-1))]),_("label",null,[Pe(_("input",{type:"radio","onUpdate:modelValue":g[2]||(g[2]=E=>n.value=E),value:2},null,512),[[zn,n.value]]),g[8]||(g[8]=Ws(" 顺序拼接(按索引顺序) ",-1))])])]),_("div",rc,[g[10]||(g[10]=_("label",null,"生成数量:",-1)),Pe(_("input",{type:"number","onUpdate:modelValue":g[3]||(g[3]=E=>r.value=E),min:1,max:i.value,disabled:l.value},null,8,ic),[[rt,r.value,void 0,{number:!0}]]),_("span",oc,"最大可生成:"+z(i.value)+" 个",1)]),_("div",lc,[g[11]||(g[11]=_("label",null,"审核图片(可选):",-1)),_("div",cc,[Pe(_("input",{type:"text","onUpdate:modelValue":g[4]||(g[4]=E=>o.value=E),placeholder:"选择审核图片",readonly:""},null,512),[[rt,o.value]]),_("button",{onClick:p},"选择图片")])]),_("div",uc,[_("button",{class:"btn-primary",onClick:O,disabled:!d.value||l.value},z(l.value?"处理中...":"开始拼接"),9,fc)]),u.value.length>0?(J(),Q("div",ac,[g[13]||(g[13]=_("h3",null,"拼接结果",-1)),_("table",null,[g[12]||(g[12]=_("thead",null,[_("tr",null,[_("th",null,"序号"),_("th",null,"文件名"),_("th",null,"大小"),_("th",null,"时长"),_("th",null,"状态"),_("th",null,"进度")])],-1)),_("tbody",null,[(J(!0),Q(de,null,Hs(u.value,E=>(J(),Q("tr",{key:E.index},[_("td",null,z(E.index),1),_("td",null,z(E.fileName),1),_("td",null,z(E.size),1),_("td",null,z(E.seconds)+"秒",1),_("td",{class:pt(E.status==="拼接成功"?"success":"error")},z(E.status),3),_("td",null,[_("div",dc,[_("div",{class:"progress-fill",style:ds({width:E.progress})},null,4),_("span",hc,z(E.progress),1)])])]))),128))])])])):ge("",!0)]))}},gc=ys(pc,[["__scopeId","data-v-976c97bb"]]),mc={class:"extract-tab"},vc={class:"form-group"},_c={class:"input-group"},bc={key:0,class:"form-group"},yc={class:"video-list"},xc={class:"hint"},Sc={class:"form-group"},wc=["disabled"],Cc={class:"form-group"},Tc=["disabled"],Ec=["disabled"],Oc={key:1,class:"help-info"},Pc={key:2,class:"results"},Ac={class:"result-summary"},Ic={__name:"ExtractTab",setup(e){const t=Y(""),s=Y([]),n=Y(1),r=Y(!1),i=Y(""),o=Y([]),l=ct(()=>t.value&&s.value.length>0&&n.value>0&&!r.value),u=ct(()=>t.value&&s.value.length>0&&!r.value),d=ct(()=>o.value.filter(g=>g.success).length),a=ct(()=>o.value.filter(g=>!g.success).length),p=async()=>{try{const g=await ri();if(g&&g.trim()){t.value=g.trim();try{s.value=await Vl(t.value)}catch(E){alert("列出视频失败: "+E.message)}}}catch(g){console.error("选择文件夹失败:",g),alert("选择文件夹失败: "+(g.message||"未知错误"))}},w=g=>g.split("/").pop()||g.split("\\").pop()||g,O=async()=>{if(l.value){r.value=!0,o.value=[],i.value=`开始处理,每个视频将生成 ${n.value} 个抽帧视频...`;try{const g={folderPath:t.value,extractCount:n.value},E=s.value.length*n.value;i.value=`处理中... (0/${E})`;try{const $=await Hl(g);$&&(o.value=$,i.value=`全部完成! 共处理 ${E} 个任务,成功 ${d.value} 个,失败 ${a.value} 个`)}catch($){alert("抽帧失败: "+$.message),i.value="处理失败"}}catch(g){alert("抽帧失败: "+g.message),i.value="处理失败: "+g.message}finally{r.value=!1}}},P=async()=>{if(u.value){r.value=!0,o.value=[],i.value="开始修改元数据...";try{if(extractService&&extractService.ModifyVideosMetadata){const g=s.value.length;i.value=`处理中... (0/${g})`;const E=await extractService.ModifyVideosMetadata(t.value);E&&(o.value=E,i.value=`全部完成! 共处理 ${g} 个任务,成功 ${d.value} 个,失败 ${a.value} 个`)}}catch(g){alert("修改失败: "+g.message),i.value="处理失败: "+g.message}finally{r.value=!1}}};return(g,E)=>(J(),Q("div",mc,[E[6]||(E[6]=_("h2",null,"视频抽帧",-1)),_("div",vc,[E[2]||(E[2]=_("label",null,"选择文件夹:",-1)),_("div",_c,[Pe(_("input",{type:"text","onUpdate:modelValue":E[0]||(E[0]=$=>t.value=$),placeholder:"请选择包含视频文件的目录",readonly:""},null,512),[[rt,t.value]]),_("button",{onClick:p},"选择文件夹")])]),s.value.length>0?(J(),Q("div",bc,[E[3]||(E[3]=_("label",null,"视频文件:",-1)),_("div",yc,[(J(!0),Q(de,null,Hs(s.value,($,V)=>(J(),Q("div",{key:V,class:"video-item"},z(w($)),1))),128))]),_("p",xc,"共 "+z(s.value.length)+" 个视频文件",1)])):ge("",!0),_("div",Sc,[E[4]||(E[4]=_("label",null,"每个视频生成数量:",-1)),Pe(_("input",{type:"number","onUpdate:modelValue":E[1]||(E[1]=$=>n.value=$),min:1,disabled:r.value},null,8,wc),[[rt,n.value,void 0,{number:!0}]])]),_("div",Cc,[_("button",{class:"btn-primary",onClick:O,disabled:!l.value||r.value},z(r.value?"处理中...":"开始抽帧"),9,Tc),_("button",{class:"btn-secondary",onClick:P,disabled:!u.value||r.value,style:{"margin-left":"10px"}},z(r.value?"处理中...":"开始修改元数据"),9,Ec)]),i.value?(J(),Q("div",Oc,z(i.value),1)):ge("",!0),o.value.length>0?(J(),Q("div",Pc,[E[5]||(E[5]=_("h3",null,"处理结果",-1)),_("div",Ac,[_("p",null,"成功: "+z(d.value)+" 个",1),_("p",null,"失败: "+z(a.value)+" 个",1)])])):ge("",!0)]))}},Mc=ys(Ic,[["__scopeId","data-v-32ae0b7c"]]),Fc={class:"dialog-header"},Rc={class:"dialog-body"},$c={class:"form-group"},Dc={class:"form-group"},Lc={key:0,class:"error-message"},Nc={class:"dialog-footer"},jc=["disabled"],Hc={__name:"LoginDialog",props:{allowClose:{type:Boolean,default:!1}},emits:["close","login-success"],setup(e,{emit:t}){const s=e,n=t,r=Y(""),i=Y(""),o=Y(!1),l=Y(""),u=()=>{s.allowClose&&n("close")},d=async()=>{if(!r.value||!i.value){l.value="请输入用户名和密码";return}o.value=!0,l.value="";try{const a=await Ll(r.value,i.value);a&&a.Code===200?(n("login-success"),n("close")):l.value=a&&a.Msg||"登录失败"}catch(a){l.value="登录失败: "+(a.message||a)}finally{o.value=!1}};return(a,p)=>(J(),Q("div",{class:"dialog-overlay",onClick:u},[_("div",{class:"dialog",onClick:p[4]||(p[4]=Pl(()=>{},["stop"]))},[_("div",Fc,[p[5]||(p[5]=_("h3",null,"用户登录",-1)),e.allowClose?(J(),Q("button",{key:0,class:"close-btn",onClick:p[0]||(p[0]=w=>a.$emit("close"))},"×")):ge("",!0)]),_("div",Rc,[_("div",$c,[p[6]||(p[6]=_("label",null,"用户名:",-1)),Pe(_("input",{type:"text","onUpdate:modelValue":p[1]||(p[1]=w=>r.value=w),placeholder:"请输入用户名"},null,512),[[rt,r.value]])]),_("div",Dc,[p[7]||(p[7]=_("label",null,"密码:",-1)),Pe(_("input",{type:"password","onUpdate:modelValue":p[2]||(p[2]=w=>i.value=w),placeholder:"请输入密码"},null,512),[[rt,i.value]])]),l.value?(J(),Q("div",Lc,z(l.value),1)):ge("",!0)]),_("div",Nc,[e.allowClose?(J(),Q("button",{key:0,class:"btn-secondary",onClick:p[3]||(p[3]=w=>a.$emit("close"))},"取消")):ge("",!0),_("button",{class:"btn-primary",onClick:d,disabled:o.value},z(o.value?"登录中...":"登录"),9,jc)])])]))}},Vc=ys(Hc,[["__scopeId","data-v-f054741d"]]),Uc={class:"app-container"},Kc={key:0,class:"login-overlay"},Bc={class:"main-interface"},Wc={class:"sidebar"},kc={class:"menu-items"},qc={class:"main-content"},Jc={__name:"App",setup(e){const t=Y("video"),s=Y(!1),n=Y(!1),r=()=>{const u=localStorage.getItem("isLoggedIn"),d=localStorage.getItem("loginTime");if(u==="true"&&d){const a=Date.now(),p=parseInt(d);(a-p)/(1e3*60*60)<24?n.value=!0:(localStorage.removeItem("isLoggedIn"),localStorage.removeItem("loginTime"),n.value=!1,s.value=!0)}else n.value=!1,s.value=!0},i=()=>{n.value=!0,s.value=!1,localStorage.setItem("isLoggedIn","true"),localStorage.setItem("loginTime",Date.now().toString())},o=()=>{n.value&&(s.value=!1)},l=()=>{confirm("确定要退出登录吗?")&&(n.value=!1,s.value=!0,localStorage.removeItem("isLoggedIn"),localStorage.removeItem("loginTime"))};return Rr(()=>{r()}),(u,d)=>(J(),Q("div",Uc,[n.value?ge("",!0):(J(),Q("div",Kc,[...d[2]||(d[2]=[_("div",{class:"login-message"},[_("h2",null,"请先登录"),_("p",null,"您需要登录后才能使用此应用")],-1)])])),Pe(_("div",Bc,[_("div",Wc,[_("div",kc,[_("button",{class:pt(["menu-item",{active:t.value==="video"}]),onClick:d[0]||(d[0]=a=>t.value="video")},[...d[3]||(d[3]=[_("span",{class:"icon"},"🎬",-1),_("span",null,"视频",-1)])],2),d[5]||(d[5]=_("div",{class:"separator"},null,-1)),_("button",{class:pt(["menu-item",{active:t.value==="extract"}]),onClick:d[1]||(d[1]=a=>t.value="extract")},[...d[4]||(d[4]=[_("span",{class:"icon"},"✂️",-1),_("span",null,"抽帧",-1)])],2)]),_("div",{class:"menu-bottom"},[_("button",{class:"menu-item",onClick:l},[...d[6]||(d[6]=[_("span",{class:"icon"},"👤",-1)])])])]),_("div",qc,[t.value==="video"?(J(),Xt(gc,{key:0})):ge("",!0),t.value==="extract"?(J(),Xt(Mc,{key:1})):ge("",!0)])],512),[[fl,n.value]]),s.value?(J(),Xt(Vc,{key:1,"allow-close":n.value,onClose:o,onLoginSuccess:i},null,8,["allow-close"])):ge("",!0)]))}},Gc=ys(Jc,[["__scopeId","data-v-8a8ce913"]]);Ml(Gc).mount("#app"); diff --git a/wails/assets/index.html b/wails/assets/index.html index 4cf238a..bfabf9b 100644 --- a/wails/assets/index.html +++ b/wails/assets/index.html @@ -1,9 +1,9 @@ - - - - - - 视频拼接工具 + + + + + + 视频拼接工具 - - - - -
- - - + + + + + +
+ + + diff --git a/wails/ffmpeg_darwin.go b/wails/ffmpeg_darwin.go new file mode 100644 index 0000000..7114051 --- /dev/null +++ b/wails/ffmpeg_darwin.go @@ -0,0 +1,14 @@ +//go:build darwin + +package main + +import ( + "embed" +) + +//go:embed resources/ffmpeg/darwin/* +var embeddedFFmpeg embed.FS + +func getEmbeddedFFmpeg() embed.FS { + return embeddedFFmpeg +} diff --git a/wails/ffmpeg_default.go b/wails/ffmpeg_default.go new file mode 100644 index 0000000..bb863d6 --- /dev/null +++ b/wails/ffmpeg_default.go @@ -0,0 +1,14 @@ +//go:build !darwin && !windows && !linux + +package main + +import ( + "embed" +) + +//go:embed resources/ffmpeg +var embeddedFFmpeg embed.FS + +func getEmbeddedFFmpeg() embed.FS { + return embeddedFFmpeg +} diff --git a/wails/ffmpeg_linux.go b/wails/ffmpeg_linux.go new file mode 100644 index 0000000..fab5f34 --- /dev/null +++ b/wails/ffmpeg_linux.go @@ -0,0 +1,14 @@ +//go:build linux + +package main + +import ( + "embed" +) + +//go:embed resources/ffmpeg/linux/* +var embeddedFFmpeg embed.FS + +func getEmbeddedFFmpeg() embed.FS { + return embeddedFFmpeg +} diff --git a/wails/ffmpeg_windows.go b/wails/ffmpeg_windows.go new file mode 100644 index 0000000..23f6af0 --- /dev/null +++ b/wails/ffmpeg_windows.go @@ -0,0 +1,14 @@ +//go:build windows + +package main + +import ( + "embed" +) + +//go:embed resources/ffmpeg/windows/* +var embeddedFFmpeg embed.FS + +func getEmbeddedFFmpeg() embed.FS { + return embeddedFFmpeg +} diff --git a/wails/main.go b/wails/main.go index d7812c8..df54c77 100644 --- a/wails/main.go +++ b/wails/main.go @@ -11,6 +11,10 @@ import ( //go:embed assets var assets embed.FS +// getEmbeddedFFmpeg 在平台特定文件中实现(ffmpeg_darwin.go, ffmpeg_windows.go, ffmpeg_linux.go, ffmpeg_default.go) +// 根据编译时的 GOOS 自动选择对应的实现 +// 注意:此函数没有在此文件中声明,而是在平台特定文件中声明和实现 + func main() { // 检测开发模式并设置日志级别 if os.Getenv("DEV") == "true" { @@ -19,6 +23,9 @@ func main() { services.LogDebug("详细日志已启用") } + // 初始化 FFmpeg 助手(传递嵌入的文件系统,根据编译平台自动选择) + services.InitFFmpegHelper(getEmbeddedFFmpeg()) + // 创建服务 services.LogDebug("开始创建服务...") videoService := services.NewVideoService() diff --git a/wails/resources/ffmpeg/.gitignore b/wails/resources/ffmpeg/.gitignore new file mode 100644 index 0000000..280f3a3 --- /dev/null +++ b/wails/resources/ffmpeg/.gitignore @@ -0,0 +1,26 @@ +# 忽略 FFmpeg 二进制文件(文件太大,通常不应提交到 Git) +# 用户需要自行下载并放置二进制文件到编译时嵌入 + +# 根目录的二进制文件(向后兼容) +ffmpeg +ffprobe +ffmpeg.exe +ffprobe.exe + +# 按平台组织的目录中的二进制文件 +darwin/ffmpeg +darwin/ffprobe +windows/ffmpeg.exe +windows/ffprobe.exe +linux/ffmpeg +linux/ffprobe + +# 但保留 README.md、.gitkeep 和目录结构 +!README.md +!.gitkeep +!darwin/ +!windows/ +!linux/ +!darwin/.gitkeep +!windows/.gitkeep +!linux/.gitkeep diff --git a/wails/resources/ffmpeg/.gitkeep b/wails/resources/ffmpeg/.gitkeep new file mode 100644 index 0000000..164da23 --- /dev/null +++ b/wails/resources/ffmpeg/.gitkeep @@ -0,0 +1 @@ +# 保留此目录在 Git 中 diff --git a/wails/resources/ffmpeg/README.md b/wails/resources/ffmpeg/README.md new file mode 100644 index 0000000..67167c8 --- /dev/null +++ b/wails/resources/ffmpeg/README.md @@ -0,0 +1,158 @@ +# FFmpeg 二进制文件放置说明 + +此目录用于存放 FFmpeg 和 FFprobe 的二进制文件。这些文件会被**嵌入到 Go 程序**中,随应用一起分发。 + +**重要**:二进制文件会被编译到最终的可执行文件中,因此文件大小会增加。建议使用静态构建版本以减小体积。 + +## 目录结构 + +### ⭐ 推荐:按平台组织(实现自动平台选择) + +``` +resources/ +└── ffmpeg/ + ├── darwin/ # macOS 版本(编译 macOS 时自动嵌入) + │ ├── ffmpeg + │ └── ffprobe + ├── windows/ # Windows 版本(编译 Windows 时自动嵌入) + │ ├── ffmpeg.exe + │ └── ffprobe.exe + └── linux/ # Linux 版本(编译 Linux 时自动嵌入) + ├── ffmpeg + └── ffprobe +``` + +**优势**: +- ✅ 每个平台只嵌入对应平台的二进制文件 +- ✅ 减小最终可执行文件大小 +- ✅ 避免跨平台打包时的混乱 +- ✅ 使用 Go 构建标签自动选择 + +### 选项 2:所有平台通用(向后兼容) + +``` +resources/ +└── ffmpeg/ + ├── ffmpeg # 当前平台的 ffmpeg(macOS/Linux) + ├── ffmpeg.exe # 或 Windows 版本 + ├── ffprobe # 当前平台的 ffprobe(macOS/Linux) + └── ffprobe.exe # 或 Windows 版本 +``` + +**注意**:这种方式会嵌入所有文件,增加不必要的体积。 + +## 获取 FFmpeg 二进制文件 + +### macOS + +```bash +# 使用 Homebrew 安装 +brew install ffmpeg + +# 复制到资源目录 +cp /opt/homebrew/bin/ffmpeg resources/ffmpeg/ffmpeg +cp /opt/homebrew/bin/ffprobe resources/ffmpeg/ffprobe + +# 或者如果是 Intel Mac +cp /usr/local/bin/ffmpeg resources/ffmpeg/ffmpeg +cp /usr/local/bin/ffprobe resources/ffmpeg/ffprobe +``` + +### Windows + +1. 下载 FFmpeg Windows 构建版本: + - 访问 https://www.gyan.dev/ffmpeg/builds/ + - 下载 ffmpeg-release-essentials.zip + - 解压后找到 `bin` 目录下的 `ffmpeg.exe` 和 `ffprobe.exe` + +2. 复制到资源目录: + ```powershell + copy ffmpeg.exe resources\ffmpeg\ffmpeg.exe + copy ffprobe.exe resources\ffmpeg\ffprobe.exe + ``` + +### Linux + +```bash +# Ubuntu/Debian +sudo apt-get install ffmpeg + +# 复制到资源目录 +cp /usr/bin/ffmpeg resources/ffmpeg/ffmpeg +cp /usr/bin/ffprobe resources/ffmpeg/ffprobe +``` + +或者下载静态构建版本: +```bash +wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz +tar xf ffmpeg-release-amd64-static.tar.xz +cp ffmpeg-*-amd64-static/ffmpeg resources/ffmpeg/ffmpeg +cp ffmpeg-*-amd64-static/ffprobe resources/ffmpeg/ffprobe +``` + +## 注意事项 + +1. **文件权限**:在 macOS/Linux 上,确保二进制文件有执行权限: + ```bash + chmod +x resources/ffmpeg/ffmpeg + chmod +x resources/ffmpeg/ffprobe + ``` + +2. **架构匹配**: + - macOS:确保使用正确的架构(Intel 或 Apple Silicon) + - Linux:确保使用正确的架构(amd64、arm64 等) + +3. **依赖库**:静态构建版本通常更好,因为不需要外部依赖库。 + +4. **打包时**:在打包应用时,确保将此目录的内容包含到应用包中。 + +## 应用程序查找顺序 + +1. **系统 PATH**:首先检查系统 PATH 中是否有 ffmpeg/ffprobe +2. **嵌入资源**:如果未找到,从嵌入的 Go 资源中提取并解压到临时目录 +3. **文件系统资源**:最后尝试从打包的资源目录查找(如果嵌入资源未找到) + +## 嵌入和使用方式 + +### 工作原理 + +1. **编译时**: + - 使用 Go 构建标签(build tags)根据目标平台自动选择嵌入文件 + - macOS 构建时:只嵌入 `resources/ffmpeg/darwin/*` + - Windows 构建时:只嵌入 `resources/ffmpeg/windows/*` + - Linux 构建时:只嵌入 `resources/ffmpeg/linux/*` + +2. **运行时**:首次使用时,从嵌入的文件系统中提取二进制文件到临时目录 + +3. **缓存**:提取的文件会缓存在临时目录中,避免重复提取 + +### 构建标签说明 + +项目使用以下文件实现平台特定嵌入: + +- `ffmpeg_darwin.go` - macOS 平台(使用 `//go:build darwin`) +- `ffmpeg_windows.go` - Windows 平台(使用 `//go:build windows`) +- `ffmpeg_linux.go` - Linux 平台(使用 `//go:build linux`) +- `ffmpeg_default.go` - 其他平台(向后兼容) + +编译时,Go 会自动只编译匹配当前平台的文件,因此: +- 在 macOS 上编译 → 只嵌入 darwin 目录的文件 +- 在 Windows 上编译 → 只嵌入 windows 目录的文件 +- 在 Linux 上编译 → 只嵌入 linux 目录的文件 + +### 注意事项 + +- **文件大小**:二进制文件会被嵌入到最终可执行文件中,会增加程序大小 +- **首次运行**:首次运行时需要提取二进制文件,可能会有短暂的延迟 +- **临时目录**:提取的文件保存在 `{临时目录}/videoconcat-ffmpeg/{进程ID}/` 中 +- **权限**:提取的文件会自动设置执行权限(Unix 系统) +- **清理**:临时文件在程序退出后通常会被系统清理,但进程异常退出时可能需要手动清理 + +## Git 提交注意事项 + +默认情况下,`.gitignore` 已配置忽略二进制文件。如果需要将二进制文件提交到 Git: + +1. 移除 `.gitignore` 中的相关规则,或 +2. 使用 `git add -f` 强制添加 + +**注意**:二进制文件通常很大(几十到几百 MB),可能不适合提交到 Git。建议使用构建脚本在编译时自动下载。 diff --git a/wails/resources/ffmpeg/darwin/.gitkeep b/wails/resources/ffmpeg/darwin/.gitkeep new file mode 100644 index 0000000..c507e5e --- /dev/null +++ b/wails/resources/ffmpeg/darwin/.gitkeep @@ -0,0 +1,5 @@ +# macOS 平台的 FFmpeg 二进制文件放置目录 +# +# 将 macOS 版本的 ffmpeg 和 ffprobe 放在此目录下: +# - ffmpeg +# - ffprobe diff --git a/wails/resources/ffmpeg/linux/.gitkeep b/wails/resources/ffmpeg/linux/.gitkeep new file mode 100644 index 0000000..c059a5a --- /dev/null +++ b/wails/resources/ffmpeg/linux/.gitkeep @@ -0,0 +1,5 @@ +# Linux 平台的 FFmpeg 二进制文件放置目录 +# +# 将 Linux 版本的 ffmpeg 和 ffprobe 放在此目录下: +# - ffmpeg +# - ffprobe diff --git a/wails/resources/ffmpeg/windows/.gitkeep b/wails/resources/ffmpeg/windows/.gitkeep new file mode 100644 index 0000000..2482af8 --- /dev/null +++ b/wails/resources/ffmpeg/windows/.gitkeep @@ -0,0 +1,5 @@ +# Windows 平台的 FFmpeg 二进制文件放置目录 +# +# 将 Windows 版本的 ffmpeg.exe 和 ffprobe.exe 放在此目录下: +# - ffmpeg.exe +# - ffprobe.exe diff --git a/wails/resources/ffmpeg/使用说明.md b/wails/resources/ffmpeg/使用说明.md new file mode 100644 index 0000000..2a022dc --- /dev/null +++ b/wails/resources/ffmpeg/使用说明.md @@ -0,0 +1,220 @@ +# FFmpeg 嵌入使用说明 + +## 快速开始 + +### 1. 下载 FFmpeg 二进制文件 + +根据你的目标平台下载对应的 FFmpeg 静态构建版本,并放置到对应的平台目录下: + +**重要**:使用平台特定目录组织,编译时会自动只嵌入对应平台的版本。 + +#### macOS (Apple Silicon 或 Intel) +```bash +# 使用 Homebrew 安装(推荐) +brew install ffmpeg + +# 复制到 macOS 专用目录 +cp /opt/homebrew/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg +cp /opt/homebrew/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe + +# 或者如果是 Intel Mac +cp /usr/local/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg +cp /usr/local/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe + +# 设置权限 +chmod +x wails/resources/ffmpeg/darwin/ffmpeg +chmod +x wails/resources/ffmpeg/darwin/ffprobe +``` + +**或者下载静态构建版本:** +```bash +# Apple Silicon +wget https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip +unzip ffmpeg-6.1.zip +cp ffmpeg wails/resources/ffmpeg/darwin/ffmpeg +cp ffprobe wails/resources/ffmpeg/darwin/ffprobe +chmod +x wails/resources/ffmpeg/darwin/ffmpeg wails/resources/ffmpeg/darwin/ffprobe +``` + +#### Windows +```powershell +# 下载 +# 访问 https://www.gyan.dev/ffmpeg/builds/ +# 下载 ffmpeg-release-essentials.zip +# 解压后复制 bin 目录下的文件到 windows 目录 + +copy ffmpeg.exe wails\resources\ffmpeg\windows\ffmpeg.exe +copy ffprobe.exe wails\resources\ffmpeg\windows\ffprobe.exe +``` + +#### Linux +```bash +# 下载静态构建版本 +wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz +tar xf ffmpeg-release-amd64-static.tar.xz +cd ffmpeg-*-amd64-static + +# 复制到 Linux 专用目录 +cp ffmpeg ../../wails/resources/ffmpeg/linux/ffmpeg +cp ffprobe ../../wails/resources/ffmpeg/linux/ffprobe +chmod +x ../../wails/resources/ffmpeg/linux/ffmpeg +chmod +x ../../wails/resources/ffmpeg/linux/ffprobe +``` + +### 2. 编译应用 + +二进制文件会在编译时根据目标平台自动选择并嵌入: + +```bash +cd wails + +# 在 macOS 上编译 macOS 版本(只嵌入 darwin/ 目录的文件) +go build -o VideoConcat + +# 交叉编译 Windows 版本(只嵌入 windows/ 目录的文件) +GOOS=windows GOARCH=amd64 go build -o VideoConcat.exe + +# 交叉编译 Linux 版本(只嵌入 linux/ 目录的文件) +GOOS=linux GOARCH=amd64 go build -o VideoConcat +``` + +**优势**: +- ✅ macOS 版本只包含 macOS 的 FFmpeg,体积更小 +- ✅ Windows 版本只包含 Windows 的 FFmpeg,体积更小 +- ✅ 交叉编译时不会混入其他平台的二进制文件 + +### 3. 运行应用 + +应用启动时会自动: +1. 检查系统 PATH 中的 ffmpeg +2. 如果未找到,从嵌入的资源中提取到临时目录 +3. 使用提取的二进制文件 + +## 工作原理 + +### 嵌入过程 + +```go +// main.go +//go:embed resources/ffmpeg +var embeddedFFmpeg embed.FS +``` + +这行代码会在编译时将所有 `resources/ffmpeg/` 目录下的文件嵌入到程序中。 + +### 运行时提取 + +应用首次启动时: +1. 检查系统是否有 ffmpeg +2. 如果没有,从嵌入的文件系统中查找匹配的二进制文件 +3. 提取到临时目录:`{临时目录}/videoconcat-ffmpeg/{进程ID}/` +4. 设置执行权限 +5. 使用提取的文件 + +### 文件查找顺序 + +1. `resources/ffmpeg/ffmpeg` 或 `resources/ffmpeg/ffmpeg.exe` +2. `resources/ffmpeg/{GOOS}/ffmpeg` 或 `resources/ffmpeg/{GOOS}/ffmpeg.exe` +3. `resources/ffmpeg/{GOOS}_{GOARCH}/ffmpeg` 等 + +## 目录结构示例 + +### ⭐ 推荐:按平台组织(自动平台选择) + +``` +wails/resources/ffmpeg/ +├── darwin/ # macOS 版本 +│ ├── ffmpeg +│ └── ffprobe +├── windows/ # Windows 版本 +│ ├── ffmpeg.exe +│ └── ffprobe.exe +├── linux/ # Linux 版本 +│ ├── ffmpeg +│ └── ffprobe +└── README.md +``` + +**编译行为**: +- 在 macOS 上编译 → 只嵌入 `darwin/` 目录 +- 在 Windows 上编译 → 只嵌入 `windows/` 目录 +- 在 Linux 上编译 → 只嵌入 `linux/` 目录 + +### 向后兼容:单目录结构 + +``` +wails/resources/ffmpeg/ +├── ffmpeg # 当前平台的 ffmpeg(不推荐) +├── ffprobe # 当前平台的 ffprobe +└── README.md +``` + +**注意**:这种方式会嵌入所有文件,不推荐使用。 + +## 注意事项 + +### 文件大小 + +- FFmpeg 静态构建版本通常 50-200 MB +- 嵌入后会增加最终可执行文件的大小 +- 考虑使用 LZMA 压缩或分平台构建来减小体积 + +### Git 提交 + +默认情况下,二进制文件被 `.gitignore` 忽略。如果需要提交: + +```bash +# 方法1:强制添加 +git add -f wails/resources/ffmpeg/ffmpeg + +# 方法2:修改 .gitignore(不推荐,文件太大) +``` + +### 权限问题 + +在 macOS/Linux 上,确保文件有执行权限: +```bash +chmod +x wails/resources/ffmpeg/ffmpeg +chmod +x wails/resources/ffmpeg/ffprobe +``` + +### 临时文件清理 + +提取的文件保存在临时目录中。如果进程异常退出,可能需要手动清理: + +```bash +# macOS/Linux +rm -rf /tmp/videoconcat-ffmpeg + +# Windows +rd /s /q %TEMP%\videoconcat-ffmpeg +``` + +## 故障排查 + +### 问题:找不到 ffmpeg + +**检查清单:** +1. 确认二进制文件已放置在 `wails/resources/ffmpeg/` 目录 +2. 检查文件名是否正确(Windows 需要 `.exe` 后缀) +3. 检查文件权限(macOS/Linux) +4. 查看应用日志,查找详细的错误信息 + +### 问题:权限被拒绝 + +**解决方案:** +```bash +chmod +x wails/resources/ffmpeg/ffmpeg +chmod +x wails/resources/ffmpeg/ffprobe +``` + +### 问题:提取失败 + +**可能原因:** +- 临时目录不可写 +- 磁盘空间不足 +- 文件损坏 + +**检查:** +- 查看应用日志 +- 检查临时目录权限和空间 diff --git a/wails/resources/ffmpeg/平台特定嵌入说明.md b/wails/resources/ffmpeg/平台特定嵌入说明.md new file mode 100644 index 0000000..f509bd4 --- /dev/null +++ b/wails/resources/ffmpeg/平台特定嵌入说明.md @@ -0,0 +1,149 @@ +# 平台特定 FFmpeg 嵌入说明 + +## 概述 + +项目使用 **Go 构建标签(Build Tags)** 实现平台特定的 FFmpeg 嵌入,确保每个平台的构建只包含对应平台的二进制文件。 + +## 工作原理 + +### 构建标签文件 + +项目包含以下平台特定文件: + +- `ffmpeg_darwin.go` - macOS 平台(`//go:build darwin`) +- `ffmpeg_windows.go` - Windows 平台(`//go:build windows`) +- `ffmpeg_linux.go` - Linux 平台(`//go:build linux`) +- `ffmpeg_default.go` - 其他平台(向后兼容) + +### 编译时的行为 + +Go 编译器会根据当前编译目标平台(`GOOS`)自动选择匹配的文件: + +| 编译平台 | 使用文件 | 嵌入目录 | 结果 | +|---------|---------|---------|------| +| `GOOS=darwin` | `ffmpeg_darwin.go` | `resources/ffmpeg/darwin/*` | 只包含 macOS 版本 | +| `GOOS=windows` | `ffmpeg_windows.go` | `resources/ffmpeg/windows/*` | 只包含 Windows 版本 | +| `GOOS=linux` | `ffmpeg_linux.go` | `resources/ffmpeg/linux/*` | 只包含 Linux 版本 | + +## 目录结构 + +``` +wails/ +├── main.go # 主文件(声明 getEmbeddedFFmpeg 函数) +├── ffmpeg_darwin.go # macOS 实现 +├── ffmpeg_windows.go # Windows 实现 +├── ffmpeg_linux.go # Linux 实现 +├── ffmpeg_default.go # 默认实现(向后兼容) +└── resources/ + └── ffmpeg/ + ├── darwin/ # macOS 二进制文件 + │ ├── ffmpeg + │ └── ffprobe + ├── windows/ # Windows 二进制文件 + │ ├── ffmpeg.exe + │ └── ffprobe.exe + └── linux/ # Linux 二进制文件 + ├── ffmpeg + └── ffprobe +``` + +## 使用方法 + +### 1. 准备二进制文件 + +将各平台的 FFmpeg 二进制文件放置到对应目录: + +```bash +# macOS +cp /opt/homebrew/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg +cp /opt/homebrew/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe + +# Windows +copy ffmpeg.exe wails\resources\ffmpeg\windows\ffmpeg.exe +copy ffprobe.exe wails\resources\ffmpeg\windows\ffprobe.exe + +# Linux +cp ffmpeg wails/resources/ffmpeg/linux/ffmpeg +cp ffprobe wails/resources/ffmpeg/linux/ffprobe +``` + +### 2. 编译不同平台 + +```bash +# macOS 版本(只嵌入 darwin/ 目录) +GOOS=darwin go build -o VideoConcat-mac + +# Windows 版本(只嵌入 windows/ 目录) +GOOS=windows GOARCH=amd64 go build -o VideoConcat.exe + +# Linux 版本(只嵌入 linux/ 目录) +GOOS=linux GOARCH=amd64 go build -o VideoConcat-linux +``` + +### 3. 验证嵌入内容 + +编译后,可以使用以下方法验证: + +```bash +# macOS/Linux +strings VideoConcat | grep -i ffmpeg | head -5 + +# Windows (使用 PowerShell) +Select-String -Path VideoConcat.exe -Pattern "ffmpeg" | Select-Object -First 5 +``` + +## 优势 + +### 1. 减小文件体积 + +- 只嵌入当前平台的二进制文件 +- 减少不必要的文件大小增加 +- 示例:如果每个平台 FFmpeg 是 50MB,使用平台特定嵌入后: + - 之前:所有平台 = 150MB(三个平台都包含) + - 现在:每个平台 = 50MB(只包含对应平台) + +### 2. 避免交叉编译问题 + +- 编译 Windows 版本时不会误嵌入 macOS 版本 +- 编译 Linux 版本时不会误嵌入 Windows 版本 +- 避免运行时找不到正确文件的问题 + +### 3. 清晰的目录组织 + +- 各平台文件分开管理 +- 易于维护和更新 +- 符合 Go 的最佳实践 + +## 故障排查 + +### 问题:编译时找不到文件 + +**原因**:对应平台的目录中没有二进制文件 + +**解决**: +```bash +# 检查文件是否存在 +ls -la wails/resources/ffmpeg/darwin/ +ls -la wails/resources/ffmpeg/windows/ +ls -la wails/resources/ffmpeg/linux/ +``` + +### 问题:交叉编译时仍嵌入所有文件 + +**原因**:使用了旧的目录结构(根目录下的文件) + +**解决**:确保使用平台特定目录(`darwin/`, `windows/`, `linux/`) + +### 问题:运行时找不到 FFmpeg + +**检查**: +1. 查看应用日志,确认 FFmpeg 查找路径 +2. 检查临时目录中是否成功提取 +3. 验证二进制文件是否有执行权限 + +## 向后兼容 + +如果不想使用平台特定目录,仍然可以使用根目录放置文件(`resources/ffmpeg/ffmpeg`),但这样会: +- 嵌入所有平台的二进制文件 +- 增加不必要的文件大小 +- 不推荐使用 diff --git a/wails/services/extract_service.go b/wails/services/extract_service.go index 3f86865..3269e6e 100644 --- a/wails/services/extract_service.go +++ b/wails/services/extract_service.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "os" - "os/exec" "path/filepath" "strings" "sync" @@ -54,7 +53,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string os.MkdirAll(tempDir, 0755) // 获取视频信息 - cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration:stream=codec_name,r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", inputPath) + helper := GetFFmpegHelper() + if !helper.IsProbeAvailable() { + return fmt.Errorf("ffprobe 不可用,请确保已安装 ffmpeg") + } + cmd := helper.ProbeCommand("-v", "error", "-show_entries", "format=duration:stream=codec_name,r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", inputPath) output, err := cmd.Output() if err != nil { return fmt.Errorf("获取视频信息失败: %v", err) @@ -94,8 +97,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string // 如果是 HEVC,先转换为 H.264 if codecName == "hevc" { + if !helper.IsAvailable() { + return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg") + } videoConvert := filepath.Join(tempDir, "convert.mp4") - cmd := exec.Command("ffmpeg", "-i", inputPath, "-c:v", "libx264", "-y", videoConvert) + cmd := helper.Command("-i", inputPath, "-c:v", "libx264", "-y", videoConvert) if err := cmd.Run(); err != nil { return fmt.Errorf("转换HEVC失败: %v", err) } @@ -116,13 +122,13 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string videoPart2 := filepath.Join(tempDir, "part2.mp4") // 第一部分:0 到 randomFrame - 0.016 - cmd = exec.Command("ffmpeg", "-i", inputPath, "-ss", "0", "-t", fmt.Sprintf("%.6f", randomFrame-0.016), "-c", "copy", "-y", videoPart1) + cmd = helper.Command("-i", inputPath, "-ss", "0", "-t", fmt.Sprintf("%.6f", randomFrame-0.016), "-c", "copy", "-y", videoPart1) if err := cmd.Run(); err != nil { return fmt.Errorf("裁剪第一部分失败: %v", err) } // 第二部分:randomFrame 到结束 - cmd = exec.Command("ffmpeg", "-i", inputPath, "-ss", fmt.Sprintf("%.6f", randomFrame), "-c", "copy", "-y", videoPart2) + cmd = helper.Command("-i", inputPath, "-ss", fmt.Sprintf("%.6f", randomFrame), "-c", "copy", "-y", videoPart2) if err := cmd.Run(); err != nil { return fmt.Errorf("裁剪第二部分失败: %v", err) } @@ -137,7 +143,7 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string file.WriteString(fmt.Sprintf("file '%s'\n", strings.ReplaceAll(videoPart2, "\\", "/"))) file.Close() - cmd = exec.Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", concatFile, "-c", "copy", "-y", outputPath) + cmd = helper.Command("-f", "concat", "-safe", "0", "-i", concatFile, "-c", "copy", "-y", outputPath) if err := cmd.Run(); err != nil { return fmt.Errorf("合并视频失败: %v", err) } @@ -247,8 +253,12 @@ func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequ // ModifyByMetadata 通过修改元数据改变文件 MD5 func (s *ExtractService) ModifyByMetadata(ctx context.Context, inputPath string, outputPath string) error { + helper := GetFFmpegHelper() + if !helper.IsAvailable() { + return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg") + } comment := fmt.Sprintf("JSY_%s", time.Now().Format("20060102150405")) - cmd := exec.Command("ffmpeg", "-i", inputPath, + cmd := helper.Command("-i", inputPath, "-c", "copy", "-metadata", fmt.Sprintf("comment=%s", comment), "-y", outputPath) diff --git a/wails/services/ffmpeg_helper.go b/wails/services/ffmpeg_helper.go new file mode 100644 index 0000000..646dd90 --- /dev/null +++ b/wails/services/ffmpeg_helper.go @@ -0,0 +1,383 @@ +package services + +import ( + "embed" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +// FFmpegHelper FFmpeg 工具助手 +type FFmpegHelper struct { + ffmpegPath string + ffprobePath string + embeddedFS *embed.FS // 使用指针以便检查 nil + extractedPath string // 已提取的临时目录路径 +} + +var ffmpegHelper *FFmpegHelper + +// InitFFmpegHelper 初始化 FFmpeg 助手(传入嵌入的文件系统) +func InitFFmpegHelper(embeddedFS embed.FS) { + if ffmpegHelper == nil { + ffmpegHelper = &FFmpegHelper{ + embeddedFS: &embeddedFS, + } + ffmpegHelper.init() + } +} + +// GetFFmpegHelper 获取 FFmpeg 助手实例(单例模式) +func GetFFmpegHelper() *FFmpegHelper { + if ffmpegHelper == nil { + // 如果没有初始化,创建一个不包含嵌入文件系统的实例 + ffmpegHelper = &FFmpegHelper{} + ffmpegHelper.init() + } + return ffmpegHelper +} + +// init 初始化 FFmpeg 路径 +func (h *FFmpegHelper) init() { + // 先尝试从系统 PATH 查找 + h.ffmpegPath = h.findExecutable("ffmpeg") + h.ffprobePath = h.findExecutable("ffprobe") + + // 如果系统 PATH 中没有,尝试从嵌入的资源中提取 + if h.ffmpegPath == "" && h.hasEmbeddedFS() { + h.ffmpegPath = h.extractEmbeddedBinary("ffmpeg") + } + if h.ffprobePath == "" && h.hasEmbeddedFS() { + h.ffprobePath = h.extractEmbeddedBinary("ffprobe") + } + + // 如果嵌入的资源也没有,尝试从打包的资源目录中查找 + if h.ffmpegPath == "" { + h.ffmpegPath = h.findBundledBinary("ffmpeg") + } + if h.ffprobePath == "" { + h.ffprobePath = h.findBundledBinary("ffprobe") + } + + // 记录日志 + if h.ffmpegPath != "" { + LogInfof("找到 ffmpeg: %s", h.ffmpegPath) + } else { + LogWarn("未找到 ffmpeg,视频处理功能可能无法使用") + LogWarn("请确保 ffmpeg 已安装并在系统 PATH 中,或将其嵌入到 resources/ffmpeg/ 目录下") + } + + if h.ffprobePath != "" { + LogInfof("找到 ffprobe: %s", h.ffprobePath) + } else { + LogWarn("未找到 ffprobe,视频信息获取功能可能无法使用") + } +} + +// findExecutable 从系统 PATH 中查找可执行文件 +func (h *FFmpegHelper) findExecutable(name string) string { + path, err := exec.LookPath(name) + if err == nil { + // 验证文件是否可执行 + if info, err := os.Stat(path); err == nil { + if runtime.GOOS != "windows" { + // Unix 系统检查执行权限 + if info.Mode().Perm()&0111 != 0 { + return path + } + } else { + // Windows 系统直接返回 + return path + } + } + } + return "" +} + +// findBundledBinary 从打包的资源中查找二进制文件 +func (h *FFmpegHelper) findBundledBinary(name string) string { + // 获取可执行文件所在目录 + exePath, err := os.Executable() + if err != nil { + return "" + } + + exeDir := filepath.Dir(exePath) + + // 根据操作系统确定二进制文件名 + binaryName := name + if runtime.GOOS == "windows" { + binaryName = name + ".exe" + } + + // 可能的资源目录路径(按优先级排序) + possiblePaths := []string{ + // macOS app bundle 中的路径 + filepath.Join(exeDir, "..", "Resources", "ffmpeg", binaryName), + // Windows/Linux 相对路径 + filepath.Join(exeDir, "resources", "ffmpeg", binaryName), + // 开发环境的相对路径 + filepath.Join(exeDir, "..", "resources", "ffmpeg", binaryName), + // 当前目录 + filepath.Join(".", "resources", "ffmpeg", binaryName), + // 直接在可执行文件目录 + filepath.Join(exeDir, binaryName), + } + + for _, path := range possiblePaths { + // 转换为绝对路径 + absPath, err := filepath.Abs(path) + if err != nil { + continue + } + + // 检查文件是否存在 + if info, err := os.Stat(absPath); err == nil && !info.IsDir() { + // 在首次使用时,可能需要提取到临时目录并设置执行权限 + if runtime.GOOS != "windows" { + // 确保文件有执行权限 + os.Chmod(absPath, 0755) + } + return absPath + } + } + + return "" +} + +// GetFFmpegPath 获取 ffmpeg 路径 +func (h *FFmpegHelper) GetFFmpegPath() string { + return h.ffmpegPath +} + +// GetFFprobePath 获取 ffprobe 路径 +func (h *FFmpegHelper) GetFFprobePath() string { + return h.ffprobePath +} + +// Command 创建 FFmpeg 命令 +func (h *FFmpegHelper) Command(args ...string) *exec.Cmd { + if h.ffmpegPath == "" { + return nil + } + return exec.Command(h.ffmpegPath, args...) +} + +// ProbeCommand 创建 FFprobe 命令 +func (h *FFmpegHelper) ProbeCommand(args ...string) *exec.Cmd { + if h.ffprobePath == "" { + return nil + } + return exec.Command(h.ffprobePath, args...) +} + +// IsAvailable 检查 FFmpeg 是否可用 +func (h *FFmpegHelper) IsAvailable() bool { + return h.ffmpegPath != "" +} + +// IsProbeAvailable 检查 FFprobe 是否可用 +func (h *FFmpegHelper) IsProbeAvailable() bool { + return h.ffprobePath != "" +} + +// hasEmbeddedFS 检查是否有嵌入的文件系统 +func (h *FFmpegHelper) hasEmbeddedFS() bool { + return h.embeddedFS != nil +} + +// extractEmbeddedBinary 从嵌入的文件系统中提取二进制文件 +func (h *FFmpegHelper) extractEmbeddedBinary(name string) string { + if !h.hasEmbeddedFS() { + return "" + } + + // 确定二进制文件名(根据当前平台) + binaryName := name + if runtime.GOOS == "windows" { + binaryName = name + ".exe" + } + + // 在嵌入的文件系统中查找文件 + // 根据不同的嵌入方式,可能的路径有所不同: + // - 如果使用平台特定目录:resources/ffmpeg/darwin/ffmpeg + // - 如果使用通用目录:resources/ffmpeg/ffmpeg + possiblePaths := []string{ + // 平台特定目录(推荐,用于分离打包) + fmt.Sprintf("resources/ffmpeg/%s/%s", runtime.GOOS, binaryName), + fmt.Sprintf("resources/ffmpeg/%s/%s", runtime.GOOS, name), + fmt.Sprintf("resources/ffmpeg/%s_%s/%s", runtime.GOOS, runtime.GOARCH, binaryName), + // 通用路径(向后兼容) + fmt.Sprintf("resources/ffmpeg/%s", binaryName), + fmt.Sprintf("resources/ffmpeg/%s", name), + } + + var binaryData []byte + var err error + + for _, path := range possiblePaths { + binaryData, err = h.embeddedFS.ReadFile(path) + if err == nil { + LogDebugf("从嵌入资源中找到二进制文件: %s", path) + break + } + } + + if err != nil { + LogDebugf("未在嵌入资源中找到 %s,尝试遍历目录", name) + // 如果直接路径找不到,尝试遍历目录查找 + binaryData, _ = h.findBinaryInEmbeddedFS(name) + if binaryData == nil { + return "" + } + } + + // 提取到临时目录 + path, err := h.extractToTemp(name, binaryData) + if err != nil { + LogErrorf("提取嵌入的二进制文件失败: %v", err) + return "" + } + + return path +} + +// findBinaryInEmbeddedFS 在嵌入的文件系统中查找二进制文件 +func (h *FFmpegHelper) findBinaryInEmbeddedFS(name string) ([]byte, string) { + if !h.hasEmbeddedFS() { + return nil, "" + } + + binaryName := name + if runtime.GOOS == "windows" { + binaryName = name + ".exe" + } + + // 使用结构体存储结果 + type result struct { + data []byte + path string + } + var found *result + + // 遍历 resources/ffmpeg 目录 + err := fs.WalkDir(h.embeddedFS, "resources/ffmpeg", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil // 忽略错误,继续遍历 + } + + // 检查文件名是否匹配 + if !d.IsDir() && (d.Name() == name || d.Name() == binaryName) { + data, readErr := h.embeddedFS.ReadFile(path) + if readErr == nil { + // 找到匹配的文件 + found = &result{ + data: data, + path: path, + } + return fs.SkipAll // 找到后停止遍历 + } + } + return nil + }) + + if err == nil && found != nil { + return found.data, found.path + } + + return nil, "" +} + +// extractToTemp 提取二进制文件到临时目录 +func (h *FFmpegHelper) extractToTemp(name string, data []byte) (string, error) { + // 创建临时目录(只创建一次) + if h.extractedPath == "" { + tempDir := filepath.Join(os.TempDir(), "videoconcat-ffmpeg", fmt.Sprintf("%d", os.Getpid())) + if err := os.MkdirAll(tempDir, 0755); err != nil { + return "", fmt.Errorf("创建临时目录失败: %v", err) + } + h.extractedPath = tempDir + } + + // 确定二进制文件名 + binaryName := name + if runtime.GOOS == "windows" { + binaryName = name + ".exe" + } + + // 目标路径 + targetPath := filepath.Join(h.extractedPath, binaryName) + + // 如果文件已存在且大小一致,直接返回 + if info, err := os.Stat(targetPath); err == nil { + if info.Size() == int64(len(data)) { + return targetPath, nil + } + // 文件大小不一致,删除重新写入 + os.Remove(targetPath) + } + + // 写入文件 + file, err := os.Create(targetPath) + if err != nil { + return "", fmt.Errorf("创建文件失败: %v", err) + } + defer file.Close() + + if _, err := file.Write(data); err != nil { + return "", fmt.Errorf("写入文件失败: %v", err) + } + + // 设置执行权限(Unix 系统) + if runtime.GOOS != "windows" { + if err := os.Chmod(targetPath, 0755); err != nil { + return "", fmt.Errorf("设置执行权限失败: %v", err) + } + } + + // 验证文件 + if _, err := os.Stat(targetPath); err != nil { + return "", fmt.Errorf("验证文件失败: %v", err) + } + + LogInfof("已提取嵌入的二进制文件到: %s", targetPath) + return targetPath, nil +} + +// ExtractBundledBinary 从嵌入的资源中提取二进制文件到临时目录(兼容旧接口) +func (h *FFmpegHelper) ExtractBundledBinary(name string, data []byte) (string, error) { + return h.extractToTemp(name, data) +} + +// CopyFile 复制文件(用于从嵌入资源复制到目标位置) +func (h *FFmpegHelper) CopyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("打开源文件失败: %v", err) + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return fmt.Errorf("创建目标文件失败: %v", err) + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + if err != nil { + return fmt.Errorf("复制文件失败: %v", err) + } + + // 设置执行权限(Unix 系统) + if runtime.GOOS != "windows" { + if err := os.Chmod(dst, 0755); err != nil { + return fmt.Errorf("设置执行权限失败: %v", err) + } + } + + return nil +} diff --git a/wails/services/video_service.go b/wails/services/video_service.go index c4cc0c8..e954103 100644 --- a/wails/services/video_service.go +++ b/wails/services/video_service.go @@ -8,7 +8,6 @@ import ( "io" "math/rand" "os" - "os/exec" "path/filepath" "strings" "sync" @@ -129,7 +128,11 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) { } // 使用 FFmpeg 转换 - cmd := exec.Command("ffmpeg", "-i", videoPath, + helper := GetFFmpegHelper() + if !helper.IsAvailable() { + return "", fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg") + } + cmd := helper.Command("-i", videoPath, "-c:v", "libx264", "-c:a", "aac", "-ar", "44100", @@ -143,7 +146,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) { output, err := cmd.CombinedOutput() if err != nil { // 尝试备用方案 - cmd2 := exec.Command("ffmpeg", "-i", videoPath, + cmd2 := helper.Command("-i", videoPath, "-c", "copy", "-f", "mpegts", "-y", tsPath) @@ -151,6 +154,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) { if err2 != nil { return "", fmt.Errorf("视频转换失败: %v, 输出: %s, 备用方案失败: %v, 输出: %s", err, string(output), err2, string(output2)) } + // 备用方案成功,继续执行 } return tsPath, nil @@ -343,7 +347,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) ( "-y", outputPath, } - cmd := exec.Command("ffmpeg", args...) + helper := GetFFmpegHelper() + if !helper.IsAvailable() { + LogError("ffmpeg 不可用") + result.Status = "拼接失败" + result.Progress = "失败" + mu.Lock() + results = append(results, result) + mu.Unlock() + return + } + cmd := helper.Command(args...) err = cmd.Run() if err != nil { LogError(fmt.Sprintf("拼接视频失败: %v", err)) @@ -366,7 +380,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) ( "-y", outputImgPath, } - cmd := exec.Command("ffmpeg", args...) + helper := GetFFmpegHelper() + if !helper.IsAvailable() { + LogError("ffmpeg 不可用") + result.Status = "拼接失败" + result.Progress = "失败" + mu.Lock() + results = append(results, result) + mu.Unlock() + return + } + cmd := helper.Command(args...) cmd.Run() // 忽略错误,水印是可选的 } @@ -383,12 +407,18 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) ( // 获取视频时长(使用 ffprobe) seconds := 0 - cmd = exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", outputPath) - output, err := cmd.Output() - if err == nil { - var duration float64 - fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration) - seconds = int(duration) + if helper.IsProbeAvailable() { + cmd = helper.ProbeCommand("-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", outputPath) + } else { + cmd = nil + } + if cmd != nil { + output, err := cmd.Output() + if err == nil { + var duration float64 + fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration) + seconds = int(duration) + } } sizeMB := fileInfo.Size() / 1024 / 1024