diff --git a/.gitignore b/.gitignore index 499a1f1..92438e0 100644 --- a/.gitignore +++ b/.gitignore @@ -361,4 +361,6 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +**/.DS_Store \ No newline at end of file diff --git a/wails/.gitignore b/wails/.gitignore new file mode 100644 index 0000000..ba8194a --- /dev/null +++ b/wails/.gitignore @@ -0,0 +1,6 @@ +.task +bin +frontend/dist +frontend/node_modules +build/linux/appimage/build +build/windows/nsis/MicrosoftEdgeWebview2Setup.exe \ No newline at end of file diff --git a/wails/README.md b/wails/README.md new file mode 100644 index 0000000..ad12c3f --- /dev/null +++ b/wails/README.md @@ -0,0 +1,59 @@ +# Welcome to Your New Wails3 Project! + +Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running. + +## Getting Started + +1. Navigate to your project directory in the terminal. + +2. To run your application in development mode, use the following command: + + ``` + wails3 dev + ``` + + This will start your application and enable hot-reloading for both frontend and backend changes. + +3. To build your application for production, use: + + ``` + wails3 build + ``` + + This will create a production-ready executable in the `build` directory. + +## Exploring Wails3 Features + +Now that you have your project set up, it's time to explore the features that Wails3 offers: + +1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications. + +2. **Run an example**: To run any of the examples, navigate to the example's directory and use: + + ``` + go run . + ``` + + Note: Some examples may be under development during the alpha phase. + +3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references. + +4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions). + +## Project Structure + +Take a moment to familiarize yourself with your project structure: + +- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript) +- `main.go`: The entry point of your Go backend +- `app.go`: Define your application structure and methods here +- `wails.json`: Configuration file for your Wails project + +## Next Steps + +1. Modify the frontend in the `frontend/` directory to create your desired UI. +2. Add backend functionality in `main.go`. +3. Use `wails3 dev` to see your changes in real-time. +4. When ready, build your application with `wails3 build`. + +Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community. diff --git a/wails/Taskfile.yml b/wails/Taskfile.yml new file mode 100644 index 0000000..6e17902 --- /dev/null +++ b/wails/Taskfile.yml @@ -0,0 +1,40 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + ios: ./build/ios/Taskfile.yml + android: ./build/android/Taskfile.yml + +vars: + APP_NAME: "VideoConcat" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + cmds: + - task: common:setup:docker 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-IwiMqFON.js b/wails/assets/assets/index-IwiMqFON.js new file mode 100644 index 0000000..e1ced2f --- /dev/null +++ b/wails/assets/assets/index-IwiMqFON.js @@ -0,0 +1,17 @@ +import{Create as _e,Call as vt}from"@wailsio/runtime";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function s(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(r){if(r.ep)return;r.ep=!0;const i=s(r);fetch(r.href,i)}})();/** +* @vue/shared v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Js(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const U={},ut=[],Ae=()=>{},Zn=()=>!1,us=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Gs=e=>e.startsWith("onUpdate:"),oe=Object.assign,Ys=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},li=Object.prototype.hasOwnProperty,j=(e,t)=>li.call(e,t),R=Array.isArray,ft=e=>jt(e)==="[object Map]",Qn=e=>jt(e)==="[object Set]",xn=e=>jt(e)==="[object Date]",D=e=>typeof e=="function",X=e=>typeof e=="string",Ie=e=>typeof e=="symbol",k=e=>e!==null&&typeof e=="object",er=e=>(k(e)||D(e))&&D(e.then)&&D(e.catch),tr=Object.prototype.toString,jt=e=>tr.call(e),ci=e=>jt(e).slice(8,-1),sr=e=>jt(e)==="[object Object]",zs=e=>X(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Et=Js(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),fs=e=>{const t=Object.create(null);return s=>t[s]||(t[s]=e(s))},ui=/-\w/g,Je=fs(e=>e.replace(ui,t=>t.slice(1).toUpperCase())),fi=/\B([A-Z])/g,it=fs(e=>e.replace(fi,"-$1").toLowerCase()),nr=fs(e=>e.charAt(0).toUpperCase()+e.slice(1)),ws=fs(e=>e?`on${nr(e)}`:""),qe=(e,t)=>!Object.is(e,t),Yt=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},Xs=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Sn;const as=()=>Sn||(Sn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ds(e){if(R(e)){const t={};for(let s=0;s{if(s){const n=s.split(di);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function pt(e){let t="";if(X(e))t=e;else if(R(e))for(let s=0;s!!(e&&e.__v_isRef===!0),z=e=>X(e)?e:e==null?"":R(e)||k(e)&&(e.toString===tr||!D(e.toString))?or(e)?z(e.value):JSON.stringify(e,lr,2):String(e),lr=(e,t)=>or(t)?lr(e,t.value):ft(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,r],i)=>(s[Cs(n,i)+" =>"]=r,s),{})}:Qn(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Cs(s))}:Ie(t)?Cs(t):k(t)&&!R(t)&&!sr(t)?String(t):t,Cs=(e,t="")=>{var s;return Ie(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};/** +* @vue/reactivity v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ue;class _i{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=ue,!t&&ue&&(this.index=(ue.scopes||(ue.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0&&(ue=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let s,n;for(s=0,n=this.effects.length;s0)return;if(Pt){let t=Pt;for(Pt=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;Ot;){let t=Ot;for(Ot=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function ar(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function dr(e){let t,s=e.depsTail,n=s;for(;n;){const r=n.prevDep;n.version===-1?(n===s&&(s=r),en(n),yi(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=r}e.deps=t,e.depsTail=s}function $s(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(hr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function hr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Rt)||(e.globalVersion=Rt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!$s(e))))return;e.flags|=2;const t=e.dep,s=W,n=ve;W=e,ve=!0;try{ar(e);const r=e.fn(e._value);(t.version===0||qe(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{W=s,ve=n,dr(e),e.flags&=-3}}function en(e,t=!1){const{dep:s,prevSub:n,nextSub:r}=e;if(n&&(n.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)en(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function yi(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let ve=!0;const pr=[];function Ne(){pr.push(ve),ve=!1}function je(){const e=pr.pop();ve=e===void 0?!0:e}function wn(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=W;W=void 0;try{t()}finally{W=s}}}let Rt=0;class xi{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class tn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!W||!ve||W===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==W)s=this.activeLink=new xi(W,this),W.deps?(s.prevDep=W.depsTail,W.depsTail.nextDep=s,W.depsTail=s):W.deps=W.depsTail=s,gr(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=W.depsTail,s.nextDep=void 0,W.depsTail.nextDep=s,W.depsTail=s,W.deps===s&&(W.deps=n)}return s}trigger(t){this.version++,Rt++,this.notify(t)}notify(t){Zs();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{Qs()}}}function gr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)gr(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const Ds=new WeakMap,st=Symbol(""),Ls=Symbol(""),$t=Symbol("");function ee(e,t,s){if(ve&&W){let n=Ds.get(e);n||Ds.set(e,n=new Map);let r=n.get(s);r||(n.set(s,r=new tn),r.map=n,r.key=s),r.track()}}function $e(e,t,s,n,r,i){const o=Ds.get(e);if(!o){Rt++;return}const l=u=>{u&&u.trigger()};if(Zs(),t==="clear")o.forEach(l);else{const u=R(e),d=u&&zs(s);if(u&&s==="length"){const a=Number(n);o.forEach((p,w)=>{(w==="length"||w===$t||!Ie(w)&&w>=a)&&l(p)})}else switch((s!==void 0||o.has(void 0))&&l(o.get(s)),d&&l(o.get($t)),t){case"add":u?d&&l(o.get("length")):(l(o.get(st)),ft(e)&&l(o.get(Ls)));break;case"delete":u||(l(o.get(st)),ft(e)&&l(o.get(Ls)));break;case"set":ft(e)&&l(o.get(st));break}}Qs()}function ot(e){const t=N(e);return t===e?t:(ee(t,"iterate",$t),me(e)?t:t.map(be))}function hs(e){return ee(e=N(e),"iterate",$t),e}function Be(e,t){return He(e)?nt(e)?gt(be(t)):gt(t):be(t)}const Si={__proto__:null,[Symbol.iterator](){return Es(this,Symbol.iterator,e=>Be(this,e))},concat(...e){return ot(this).concat(...e.map(t=>R(t)?ot(t):t))},entries(){return Es(this,"entries",e=>(e[1]=Be(this,e[1]),e))},every(e,t){return Fe(this,"every",e,t,void 0,arguments)},filter(e,t){return Fe(this,"filter",e,t,s=>s.map(n=>Be(this,n)),arguments)},find(e,t){return Fe(this,"find",e,t,s=>Be(this,s),arguments)},findIndex(e,t){return Fe(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Fe(this,"findLast",e,t,s=>Be(this,s),arguments)},findLastIndex(e,t){return Fe(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Fe(this,"forEach",e,t,void 0,arguments)},includes(...e){return Os(this,"includes",e)},indexOf(...e){return Os(this,"indexOf",e)},join(e){return ot(this).join(e)},lastIndexOf(...e){return Os(this,"lastIndexOf",e)},map(e,t){return Fe(this,"map",e,t,void 0,arguments)},pop(){return St(this,"pop")},push(...e){return St(this,"push",e)},reduce(e,...t){return Cn(this,"reduce",e,t)},reduceRight(e,...t){return Cn(this,"reduceRight",e,t)},shift(){return St(this,"shift")},some(e,t){return Fe(this,"some",e,t,void 0,arguments)},splice(...e){return St(this,"splice",e)},toReversed(){return ot(this).toReversed()},toSorted(e){return ot(this).toSorted(e)},toSpliced(...e){return ot(this).toSpliced(...e)},unshift(...e){return St(this,"unshift",e)},values(){return Es(this,"values",e=>Be(this,e))}};function Es(e,t,s){const n=hs(e),r=n[t]();return n!==e&&!me(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=s(i.value)),i}),r}const wi=Array.prototype;function Fe(e,t,s,n,r,i){const o=hs(e),l=o!==e&&!me(e),u=o[t];if(u!==wi[t]){const p=u.apply(e,i);return l?be(p):p}let d=s;o!==e&&(l?d=function(p,w){return s.call(this,Be(e,p),w,e)}:s.length>2&&(d=function(p,w){return s.call(this,p,w,e)}));const a=u.call(o,d,n);return l&&r?r(a):a}function Cn(e,t,s,n){const r=hs(e);let i=s;return r!==e&&(me(e)?s.length>3&&(i=function(o,l,u){return s.call(this,o,l,u,e)}):i=function(o,l,u){return s.call(this,o,Be(e,l),u,e)}),r[t](i,...n)}function Os(e,t,s){const n=N(e);ee(n,"iterate",$t);const r=n[t](...s);return(r===-1||r===!1)&&on(s[0])?(s[0]=N(s[0]),n[t](...s)):r}function St(e,t,s=[]){Ne(),Zs();const n=N(e)[t].apply(e,s);return Qs(),je(),n}const Ci=Js("__proto__,__v_isRef,__isVue"),mr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ie));function Ti(e){Ie(e)||(e=String(e));const t=N(this);return ee(t,"has",e),t.hasOwnProperty(e)}class vr{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!r;if(s==="__v_isReadonly")return r;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(r?i?Di:xr:i?yr:br).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const o=R(t);if(!r){let u;if(o&&(u=Si[s]))return u;if(s==="hasOwnProperty")return Ti}const l=Reflect.get(t,s,te(t)?t:n);if((Ie(s)?mr.has(s):Ci(s))||(r||ee(t,"get",s),i))return l;if(te(l)){const u=o&&zs(s)?l:l.value;return r&&k(u)?js(u):u}return k(l)?r?js(l):nn(l):l}}class _r extends vr{constructor(t=!1){super(!1,t)}set(t,s,n,r){let i=t[s];const o=R(t)&&zs(s);if(!this._isShallow){const d=He(i);if(!me(n)&&!He(n)&&(i=N(i),n=N(n)),!o&&te(i)&&!te(n))return d||(i.value=n),!0}const l=o?Number(s)e,qt=e=>Reflect.getPrototypeOf(e);function Ii(e,t,s){return function(...n){const r=this.__v_raw,i=N(r),o=ft(i),l=e==="entries"||e===Symbol.iterator&&o,u=e==="keys"&&o,d=r[e](...n),a=s?Ns:t?gt:be;return!t&&ee(i,"iterate",u?Ls:st),{next(){const{value:p,done:w}=d.next();return w?{value:p,done:w}:{value:l?[a(p[0]),a(p[1])]:a(p),done:w}},[Symbol.iterator](){return this}}}}function Jt(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Mi(e,t){const s={get(r){const i=this.__v_raw,o=N(i),l=N(r);e||(qe(r,l)&&ee(o,"get",r),ee(o,"get",l));const{has:u}=qt(o),d=t?Ns:e?gt:be;if(u.call(o,r))return d(i.get(r));if(u.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&ee(N(r),"iterate",st),r.size},has(r){const i=this.__v_raw,o=N(i),l=N(r);return e||(qe(r,l)&&ee(o,"has",r),ee(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,u=N(l),d=t?Ns:e?gt:be;return!e&&ee(u,"iterate",st),l.forEach((a,p)=>r.call(i,d(a),d(p),o))}};return oe(s,e?{add:Jt("add"),set:Jt("set"),delete:Jt("delete"),clear:Jt("clear")}:{add(r){!t&&!me(r)&&!He(r)&&(r=N(r));const i=N(this);return qt(i).has.call(i,r)||(i.add(r),$e(i,"add",r,r)),this},set(r,i){!t&&!me(i)&&!He(i)&&(i=N(i));const o=N(this),{has:l,get:u}=qt(o);let d=l.call(o,r);d||(r=N(r),d=l.call(o,r));const a=u.call(o,r);return o.set(r,i),d?qe(i,a)&&$e(o,"set",r,i):$e(o,"add",r,i),this},delete(r){const i=N(this),{has:o,get:l}=qt(i);let u=o.call(i,r);u||(r=N(r),u=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return u&&$e(i,"delete",r,void 0),d},clear(){const r=N(this),i=r.size!==0,o=r.clear();return i&&$e(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{s[r]=Ii(r,e,t)}),s}function sn(e,t){const s=Mi(e,t);return(n,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?n:Reflect.get(j(s,r)&&r in n?s:n,r,i)}const Fi={get:sn(!1,!1)},Ri={get:sn(!1,!0)},$i={get:sn(!0,!1)};const br=new WeakMap,yr=new WeakMap,xr=new WeakMap,Di=new WeakMap;function Li(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ni(e){return e.__v_skip||!Object.isExtensible(e)?0:Li(ci(e))}function nn(e){return He(e)?e:rn(e,!1,Oi,Fi,br)}function ji(e){return rn(e,!1,Ai,Ri,yr)}function js(e){return rn(e,!0,Pi,$i,xr)}function rn(e,t,s,n,r){if(!k(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=Ni(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?n:s);return r.set(e,l),l}function nt(e){return He(e)?nt(e.__v_raw):!!(e&&e.__v_isReactive)}function He(e){return!!(e&&e.__v_isReadonly)}function me(e){return!!(e&&e.__v_isShallow)}function on(e){return e?!!e.__v_raw:!1}function N(e){const t=e&&e.__v_raw;return t?N(t):e}function Hi(e){return!j(e,"__v_skip")&&Object.isExtensible(e)&&rr(e,"__v_skip",!0),e}const be=e=>k(e)?nn(e):e,gt=e=>k(e)?js(e):e;function te(e){return e?e.__v_isRef===!0:!1}function Y(e){return Vi(e,!1)}function Vi(e,t){return te(e)?e:new Ui(e,t)}class Ui{constructor(t,s){this.dep=new tn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:N(t),this._value=s?t:be(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||me(t)||He(t);t=n?t:N(t),qe(t,s)&&(this._rawValue=t,this._value=n?t:be(t),this.dep.trigger())}}function Ki(e){return te(e)?e.value:e}const Bi={get:(e,t,s)=>t==="__v_raw"?e:Ki(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const r=e[t];return te(r)&&!te(s)?(r.value=s,!0):Reflect.set(e,t,s,n)}};function Sr(e){return nt(e)?e:new Proxy(e,Bi)}class Wi{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new tn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Rt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&W!==this)return fr(this,!0),!0}get value(){const t=this.dep.track();return hr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ki(e,t,s=!1){let n,r;return D(e)?n=e:(n=e.get,r=e.set),new Wi(n,r,s)}const Gt={},ts=new WeakMap;let et;function qi(e,t=!1,s=et){if(s){let n=ts.get(s);n||ts.set(s,n=[]),n.push(e)}}function Ji(e,t,s=U){const{immediate:n,deep:r,once:i,scheduler:o,augmentJob:l,call:u}=s,d=M=>r?M:me(M)||r===!1||r===0?De(M,1):De(M);let a,p,w,O,P=!1,g=!1;if(te(e)?(p=()=>e.value,P=me(e)):nt(e)?(p=()=>d(e),P=!0):R(e)?(g=!0,P=e.some(M=>nt(M)||me(M)),p=()=>e.map(M=>{if(te(M))return M.value;if(nt(M))return d(M);if(D(M))return u?u(M,2):M()})):D(e)?t?p=u?()=>u(e,2):e:p=()=>{if(w){Ne();try{w()}finally{je()}}const M=et;et=a;try{return u?u(e,3,[O]):e(O)}finally{et=M}}:p=Ae,t&&r){const M=p,Z=r===!0?1/0:r;p=()=>De(M(),Z)}const E=bi(),$=()=>{a.stop(),E&&E.active&&Ys(E.effects,a)};if(i&&t){const M=t;t=(...Z)=>{M(...Z),$()}}let V=g?new Array(e.length).fill(Gt):Gt;const G=M=>{if(!(!(a.flags&1)||!a.dirty&&!M))if(t){const Z=a.run();if(r||P||(g?Z.some((Ue,ye)=>qe(Ue,V[ye])):qe(Z,V))){w&&w();const Ue=et;et=a;try{const ye=[Z,V===Gt?void 0:g&&V[0]===Gt?[]:V,O];V=Z,u?u(t,3,ye):t(...ye)}finally{et=Ue}}}else a.run()};return l&&l(G),a=new cr(p),a.scheduler=o?()=>o(G,!1):G,O=M=>qi(M,!1,a),w=a.onStop=()=>{const M=ts.get(a);if(M){if(u)u(M,4);else for(const Z of M)Z();ts.delete(a)}},t?n?G(!0):V=a.run():o?o(G.bind(null,!0),!0):a.run(),$.pause=a.pause.bind(a),$.resume=a.resume.bind(a),$.stop=$,$}function De(e,t=1/0,s){if(t<=0||!k(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,te(e))De(e.value,t,s);else if(R(e))for(let n=0;n{De(n,t,s)});else if(sr(e)){for(const n in e)De(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&De(e[n],t,s)}return e}/** +* @vue/runtime-core v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Ht(e,t,s,n){try{return n?e(...n):e()}catch(r){ps(r,t,s)}}function Me(e,t,s,n){if(D(e)){const r=Ht(e,t,s,n);return r&&er(r)&&r.catch(i=>{ps(i,t,s)}),r}if(R(e)){const r=[];for(let i=0;i>>1,r=re[n],i=Dt(r);i=Dt(s)?re.push(e):re.splice(zi(t),0,e),e.flags|=1,Cr()}}function Cr(){ss||(ss=wr.then(Er))}function Xi(e){R(e)?at.push(...e):We&&e.id===-1?We.splice(lt+1,0,e):e.flags&1||(at.push(e),e.flags|=1),Cr()}function Tn(e,t,s=Ee+1){for(;sDt(s)-Dt(n));if(at.length=0,We){We.push(...t);return}for(We=t,lt=0;lte.id==null?e.flags&2?-1:1/0:e.id;function Er(e){try{for(Ee=0;Ee{n._d&&Dn(-1);const i=ns(t);let o;try{o=e(...r)}finally{ns(i),n._d&&Dn(1)}return o};return n._n=!0,n._c=!0,n._d=!0,n}function Pe(e,t){if(pe===null)return e;const s=_s(pe),n=e.dirs||(e.dirs=[]);for(let r=0;r1)return s&&D(t)?t.call(n&&n.proxy):t}}const eo=Symbol.for("v-scx"),to=()=>zt(eo);function Ps(e,t,s){return Pr(e,t,s)}function Pr(e,t,s=U){const{immediate:n,deep:r,flush:i,once:o}=s,l=oe({},s),u=t&&n||!t&&i!=="post";let d;if(Nt){if(i==="sync"){const O=to();d=O.__watcherHandles||(O.__watcherHandles=[])}else if(!u){const O=()=>{};return O.stop=Ae,O.resume=Ae,O.pause=Ae,O}}const a=ie;l.call=(O,P,g)=>Me(O,a,P,g);let p=!1;i==="post"?l.scheduler=O=>{ae(O,a&&a.suspense)}:i!=="sync"&&(p=!0,l.scheduler=(O,P)=>{P?O():ln(O)}),l.augmentJob=O=>{t&&(O.flags|=4),p&&(O.flags|=2,a&&(O.id=a.uid,O.i=a))};const w=Ji(e,t,l);return Nt&&(d?d.push(w):u&&w()),w}function so(e,t,s){const n=this.proxy,r=X(e)?e.includes(".")?Ar(n,e):()=>n[e]:e.bind(n,n);let i;D(t)?i=t:(i=t.handler,s=t);const o=Vt(this),l=Pr(r,i.bind(n),s);return o(),l}function Ar(e,t){const s=t.split(".");return()=>{let n=e;for(let r=0;re.__isTeleport,io=Symbol("_leaveCb");function cn(e,t){e.shapeFlag&6&&e.component?(e.transition=t,cn(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Ir(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const rs=new WeakMap;function At(e,t,s,n,r=!1){if(R(e)){e.forEach((P,g)=>At(P,t&&(R(t)?t[g]:t),s,n,r));return}if(It(n)&&!r){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&At(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?_s(n.component):n.el,o=r?null:i,{i:l,r:u}=e,d=t&&t.r,a=l.refs===U?l.refs={}:l.refs,p=l.setupState,w=N(p),O=p===U?Zn:P=>j(w,P);if(d!=null&&d!==u){if(En(t),X(d))a[d]=null,O(d)&&(p[d]=null);else if(te(d)){d.value=null;const P=t;P.k&&(a[P.k]=null)}}if(D(u))Ht(u,l,12,[o,a]);else{const P=X(u),g=te(u);if(P||g){const E=()=>{if(e.f){const $=P?O(u)?p[u]:a[u]:u.value;if(r)R($)&&Ys($,i);else if(R($))$.includes(i)||$.push(i);else if(P)a[u]=[i],O(u)&&(p[u]=a[u]);else{const V=[i];u.value=V,e.k&&(a[e.k]=V)}}else P?(a[u]=o,O(u)&&(p[u]=o)):g&&(u.value=o,e.k&&(a[e.k]=o))};if(o){const $=()=>{E(),rs.delete(e)};$.id=-1,rs.set(e,$),ae($,s)}else En(e),E()}}}function En(e){const t=rs.get(e);t&&(t.flags|=8,rs.delete(e))}as().requestIdleCallback;as().cancelIdleCallback;const It=e=>!!e.type.__asyncLoader,Mr=e=>e.type.__isKeepAlive;function oo(e,t){Fr(e,"a",t)}function lo(e,t){Fr(e,"da",t)}function Fr(e,t,s=ie){const n=e.__wdc||(e.__wdc=()=>{let r=s;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(gs(t,n,s),s){let r=s.parent;for(;r&&r.parent;)Mr(r.parent.vnode)&&co(n,t,s,r),r=r.parent}}function co(e,t,s,n){const r=gs(t,e,n,!0);$r(()=>{Ys(n[t],r)},s)}function gs(e,t,s=ie,n=!1){if(s){const r=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...o)=>{Ne();const l=Vt(s),u=Me(t,s,e,o);return l(),je(),u});return n?r.unshift(i):r.push(i),i}}const Ve=e=>(t,s=ie)=>{(!Nt||e==="sp")&&gs(e,(...n)=>t(...n),s)},uo=Ve("bm"),Rr=Ve("m"),fo=Ve("bu"),ao=Ve("u"),ho=Ve("bum"),$r=Ve("um"),po=Ve("sp"),go=Ve("rtg"),mo=Ve("rtc");function vo(e,t=ie){gs("ec",e,t)}const _o=Symbol.for("v-ndc");function Hs(e,t,s,n){let r;const i=s,o=R(e);if(o||X(e)){const l=o&&nt(e);let u=!1,d=!1;l&&(u=!me(e),d=He(e),e=hs(e)),r=new Array(e.length);for(let a=0,p=e.length;at(l,u,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let u=0,d=l.length;ue?ei(e)?_s(e):Vs(e.parent):null,Mt=oe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Vs(e.parent),$root:e=>Vs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Lr(e),$forceUpdate:e=>e.f||(e.f=()=>{ln(e.update)}),$nextTick:e=>e.n||(e.n=Yi.bind(e.proxy)),$watch:e=>so.bind(e)}),As=(e,t)=>e!==U&&!e.__isScriptSetup&&j(e,t),bo={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:r,props:i,accessCache:o,type:l,appContext:u}=e;if(t[0]!=="$"){const w=o[t];if(w!==void 0)switch(w){case 1:return n[t];case 2:return r[t];case 4:return s[t];case 3:return i[t]}else{if(As(n,t))return o[t]=1,n[t];if(r!==U&&j(r,t))return o[t]=2,r[t];if(j(i,t))return o[t]=3,i[t];if(s!==U&&j(s,t))return o[t]=4,s[t];Us&&(o[t]=0)}}const d=Mt[t];let a,p;if(d)return t==="$attrs"&&ee(e.attrs,"get",""),d(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(s!==U&&j(s,t))return o[t]=4,s[t];if(p=u.config.globalProperties,j(p,t))return p[t]},set({_:e},t,s){const{data:n,setupState:r,ctx:i}=e;return As(r,t)?(r[t]=s,!0):n!==U&&j(n,t)?(n[t]=s,!0):j(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:r,props:i,type:o}},l){let u;return!!(s[l]||e!==U&&l[0]!=="$"&&j(e,l)||As(t,l)||j(i,l)||j(n,l)||j(Mt,l)||j(r.config.globalProperties,l)||(u=o.__cssModules)&&u[l])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:j(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function On(e){return R(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let Us=!0;function yo(e){const t=Lr(e),s=e.proxy,n=e.ctx;Us=!1,t.beforeCreate&&Pn(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:u,inject:d,created:a,beforeMount:p,mounted:w,beforeUpdate:O,updated:P,activated:g,deactivated:E,beforeDestroy:$,beforeUnmount:V,destroyed:G,unmounted:M,render:Z,renderTracked:Ue,renderTriggered:ye,errorCaptured:Ke,serverPrefetch:Ut,expose:Ye,inheritAttrs:_t,components:Kt,directives:Bt,filters:xs}=t;if(d&&xo(d,n,null),o)for(const q in o){const K=o[q];D(K)&&(n[q]=K.bind(s))}if(r){const q=r.call(s,s);k(q)&&(e.data=nn(q))}if(Us=!0,i)for(const q in i){const K=i[q],ze=D(K)?K.bind(s,s):D(K.get)?K.get.bind(s,s):Ae,Wt=!D(K)&&D(K.set)?K.set.bind(s):Ae,Xe=ct({get:ze,set:Wt});Object.defineProperty(n,q,{enumerable:!0,configurable:!0,get:()=>Xe.value,set:xe=>Xe.value=xe})}if(l)for(const q in l)Dr(l[q],n,s,q);if(u){const q=D(u)?u.call(s):u;Reflect.ownKeys(q).forEach(K=>{Qi(K,q[K])})}a&&Pn(a,e,"c");function se(q,K){R(K)?K.forEach(ze=>q(ze.bind(s))):K&&q(K.bind(s))}if(se(uo,p),se(Rr,w),se(fo,O),se(ao,P),se(oo,g),se(lo,E),se(vo,Ke),se(mo,Ue),se(go,ye),se(ho,V),se($r,M),se(po,Ut),R(Ye))if(Ye.length){const q=e.exposed||(e.exposed={});Ye.forEach(K=>{Object.defineProperty(q,K,{get:()=>s[K],set:ze=>s[K]=ze,enumerable:!0})})}else e.exposed||(e.exposed={});Z&&e.render===Ae&&(e.render=Z),_t!=null&&(e.inheritAttrs=_t),Kt&&(e.components=Kt),Bt&&(e.directives=Bt),Ut&&Ir(e)}function xo(e,t,s=Ae){R(e)&&(e=Ks(e));for(const n in e){const r=e[n];let i;k(r)?"default"in r?i=zt(r.from||n,r.default,!0):i=zt(r.from||n):i=zt(r),te(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[n]=i}}function Pn(e,t,s){Me(R(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function Dr(e,t,s,n){let r=n.includes(".")?Ar(s,n):()=>s[n];if(X(e)){const i=t[e];D(i)&&Ps(r,i)}else if(D(e))Ps(r,e.bind(s));else if(k(e))if(R(e))e.forEach(i=>Dr(i,t,s,n));else{const i=D(e.handler)?e.handler.bind(s):t[e.handler];D(i)&&Ps(r,i,e)}}function Lr(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let u;return l?u=l:!r.length&&!s&&!n?u=t:(u={},r.length&&r.forEach(d=>is(u,d,o,!0)),is(u,t,o)),k(t)&&i.set(t,u),u}function is(e,t,s,n=!1){const{mixins:r,extends:i}=t;i&&is(e,i,s,!0),r&&r.forEach(o=>is(e,o,s,!0));for(const o in t)if(!(n&&o==="expose")){const l=So[o]||s&&s[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const So={data:An,props:In,emits:In,methods:Tt,computed:Tt,beforeCreate:ne,created:ne,beforeMount:ne,mounted:ne,beforeUpdate:ne,updated:ne,beforeDestroy:ne,beforeUnmount:ne,destroyed:ne,unmounted:ne,activated:ne,deactivated:ne,errorCaptured:ne,serverPrefetch:ne,components:Tt,directives:Tt,watch:Co,provide:An,inject:wo};function An(e,t){return t?e?function(){return oe(D(e)?e.call(this,this):e,D(t)?t.call(this,this):t)}:t:e}function wo(e,t){return Tt(Ks(e),Ks(t))}function Ks(e){if(R(e)){const t={};for(let s=0;st==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Je(t)}Modifiers`]||e[`${it(t)}Modifiers`];function Po(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||U;let r=s;const i=t.startsWith("update:"),o=i&&Oo(n,t.slice(7));o&&(o.trim&&(r=s.map(a=>X(a)?a.trim():a)),o.number&&(r=s.map(Xs)));let l,u=n[l=ws(t)]||n[l=ws(Je(t))];!u&&i&&(u=n[l=ws(it(t))]),u&&Me(u,e,6,r);const d=n[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Me(d,e,6,r)}}const Ao=new WeakMap;function jr(e,t,s=!1){const n=s?Ao:t.emitsCache,r=n.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!D(e)){const u=d=>{const a=jr(d,t,!0);a&&(l=!0,oe(o,a))};!s&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!i&&!l?(k(e)&&n.set(e,null),null):(R(i)?i.forEach(u=>o[u]=null):oe(o,i),k(e)&&n.set(e,o),o)}function ms(e,t){return!e||!us(t)?!1:(t=t.slice(2).replace(/Once$/,""),j(e,t[0].toLowerCase()+t.slice(1))||j(e,it(t))||j(e,t))}function Mn(e){const{type:t,vnode:s,proxy:n,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:u,render:d,renderCache:a,props:p,data:w,setupState:O,ctx:P,inheritAttrs:g}=e,E=ns(e);let $,V;try{if(s.shapeFlag&4){const M=r||n,Z=M;$=Oe(d.call(Z,M,a,p,O,w,P)),V=l}else{const M=t;$=Oe(M.length>1?M(p,{attrs:l,slots:o,emit:u}):M(p,null)),V=t.props?l:Io(l)}}catch(M){Ft.length=0,ps(M,e,1),$=Le(Ge)}let G=$;if(V&&g!==!1){const M=Object.keys(V),{shapeFlag:Z}=G;M.length&&Z&7&&(i&&M.some(Gs)&&(V=Mo(V,i)),G=mt(G,V,!1,!0))}return s.dirs&&(G=mt(G,null,!1,!0),G.dirs=G.dirs?G.dirs.concat(s.dirs):s.dirs),s.transition&&cn(G,s.transition),$=G,ns(E),$}const Io=e=>{let t;for(const s in e)(s==="class"||s==="style"||us(s))&&((t||(t={}))[s]=e[s]);return t},Mo=(e,t)=>{const s={};for(const n in e)(!Gs(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function Fo(e,t,s){const{props:n,children:r,component:i}=e,{props:o,children:l,patchFlag:u}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&u>=0){if(u&1024)return!0;if(u&16)return n?Fn(n,o,d):!!o;if(u&8){const a=t.dynamicProps;for(let p=0;pObject.create(Hr),Ur=e=>Object.getPrototypeOf(e)===Hr;function $o(e,t,s,n=!1){const r={},i=Vr();e.propsDefaults=Object.create(null),Kr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);s?e.props=n?r:ji(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Do(e,t,s,n){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=N(r),[u]=e.propsOptions;let d=!1;if((n||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let p=0;p{u=!0;const[w,O]=Br(p,t,!0);oe(o,w),O&&l.push(...O)};!s&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!u)return k(e)&&n.set(e,ut),ut;if(R(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",fn=e=>R(e)?e.map(Oe):[Oe(e)],No=(e,t,s)=>{if(t._n)return t;const n=Zi((...r)=>fn(t(...r)),s);return n._c=!1,n},Wr=(e,t,s)=>{const n=e._ctx;for(const r in e){if(un(r))continue;const i=e[r];if(D(i))t[r]=No(r,i,n);else if(i!=null){const o=fn(i);t[r]=()=>o}}},kr=(e,t)=>{const s=fn(t);e.slots.default=()=>s},qr=(e,t,s)=>{for(const n in t)(s||!un(n))&&(e[n]=t[n])},jo=(e,t,s)=>{const n=e.slots=Vr();if(e.vnode.shapeFlag&32){const r=t._;r?(qr(n,t,s),s&&rr(n,"_",r,!0)):Wr(t,n)}else t&&kr(e,t)},Ho=(e,t,s)=>{const{vnode:n,slots:r}=e;let i=!0,o=U;if(n.shapeFlag&32){const l=t._;l?s&&l===1?i=!1:qr(r,t,s):(i=!t.$stable,Wr(t,r)),o=t}else t&&(kr(e,t),o={default:1});if(i)for(const l in r)!un(l)&&o[l]==null&&delete r[l]},ae=Wo;function Vo(e){return Uo(e)}function Uo(e,t){const s=as();s.__VUE__=!0;const{insert:n,remove:r,patchProp:i,createElement:o,createText:l,createComment:u,setText:d,setElementText:a,parentNode:p,nextSibling:w,setScopeId:O=Ae,insertStaticContent:P}=e,g=(c,f,h,y=null,m=null,v=null,C=void 0,S=null,x=!!f.dynamicChildren)=>{if(c===f)return;c&&!wt(c,f)&&(y=kt(c),xe(c,m,v,!0),c=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:b,ref:I,shapeFlag:T}=f;switch(b){case vs:E(c,f,h,y);break;case Ge:$(c,f,h,y);break;case Ms:c==null&&V(f,h,y,C);break;case de:Kt(c,f,h,y,m,v,C,S,x);break;default:T&1?Z(c,f,h,y,m,v,C,S,x):T&6?Bt(c,f,h,y,m,v,C,S,x):(T&64||T&128)&&b.process(c,f,h,y,m,v,C,S,x,yt)}I!=null&&m?At(I,c&&c.ref,v,f||c,!f):I==null&&c&&c.ref!=null&&At(c.ref,null,v,c,!0)},E=(c,f,h,y)=>{if(c==null)n(f.el=l(f.children),h,y);else{const m=f.el=c.el;f.children!==c.children&&d(m,f.children)}},$=(c,f,h,y)=>{c==null?n(f.el=u(f.children||""),h,y):f.el=c.el},V=(c,f,h,y)=>{[c.el,c.anchor]=P(c.children,f,h,y,c.el,c.anchor)},G=({el:c,anchor:f},h,y)=>{let m;for(;c&&c!==f;)m=w(c),n(c,h,y),c=m;n(f,h,y)},M=({el:c,anchor:f})=>{let h;for(;c&&c!==f;)h=w(c),r(c),c=h;r(f)},Z=(c,f,h,y,m,v,C,S,x)=>{if(f.type==="svg"?C="svg":f.type==="math"&&(C="mathml"),c==null)Ue(f,h,y,m,v,C,S,x);else{const b=c.el&&c.el._isVueCE?c.el:null;try{b&&b._beginPatch(),Ut(c,f,m,v,C,S,x)}finally{b&&b._endPatch()}}},Ue=(c,f,h,y,m,v,C,S)=>{let x,b;const{props:I,shapeFlag:T,transition:A,dirs:F}=c;if(x=c.el=o(c.type,v,I&&I.is,I),T&8?a(x,c.children):T&16&&Ke(c.children,x,null,y,m,Is(c,v),C,S),F&&Ze(c,null,y,"created"),ye(x,c,c.scopeId,C,y),I){for(const B in I)B!=="value"&&!Et(B)&&i(x,B,null,I[B],v,y);"value"in I&&i(x,"value",null,I.value,v),(b=I.onVnodeBeforeMount)&&Te(b,y,c)}F&&Ze(c,null,y,"beforeMount");const L=Ko(m,A);L&&A.beforeEnter(x),n(x,f,h),((b=I&&I.onVnodeMounted)||L||F)&&ae(()=>{b&&Te(b,y,c),L&&A.enter(x),F&&Ze(c,null,y,"mounted")},m)},ye=(c,f,h,y,m)=>{if(h&&O(c,h),y)for(let v=0;v{for(let b=x;b{const S=f.el=c.el;let{patchFlag:x,dynamicChildren:b,dirs:I}=f;x|=c.patchFlag&16;const T=c.props||U,A=f.props||U;let F;if(h&&Qe(h,!1),(F=A.onVnodeBeforeUpdate)&&Te(F,h,f,c),I&&Ze(f,c,h,"beforeUpdate"),h&&Qe(h,!0),(T.innerHTML&&A.innerHTML==null||T.textContent&&A.textContent==null)&&a(S,""),b?Ye(c.dynamicChildren,b,S,h,y,Is(f,m),v):C||K(c,f,S,null,h,y,Is(f,m),v,!1),x>0){if(x&16)_t(S,T,A,h,m);else if(x&2&&T.class!==A.class&&i(S,"class",null,A.class,m),x&4&&i(S,"style",T.style,A.style,m),x&8){const L=f.dynamicProps;for(let B=0;B{F&&Te(F,h,f,c),I&&Ze(f,c,h,"updated")},y)},Ye=(c,f,h,y,m,v,C)=>{for(let S=0;S{if(f!==h){if(f!==U)for(const v in f)!Et(v)&&!(v in h)&&i(c,v,f[v],null,m,y);for(const v in h){if(Et(v))continue;const C=h[v],S=f[v];C!==S&&v!=="value"&&i(c,v,S,C,m,y)}"value"in h&&i(c,"value",f.value,h.value,m)}},Kt=(c,f,h,y,m,v,C,S,x)=>{const b=f.el=c?c.el:l(""),I=f.anchor=c?c.anchor:l("");let{patchFlag:T,dynamicChildren:A,slotScopeIds:F}=f;F&&(S=S?S.concat(F):F),c==null?(n(b,h,y),n(I,h,y),Ke(f.children||[],h,I,m,v,C,S,x)):T>0&&T&64&&A&&c.dynamicChildren&&c.dynamicChildren.length===A.length?(Ye(c.dynamicChildren,A,h,m,v,C,S),(f.key!=null||m&&f===m.subTree)&&Jr(c,f,!0)):K(c,f,h,I,m,v,C,S,x)},Bt=(c,f,h,y,m,v,C,S,x)=>{f.slotScopeIds=S,c==null?f.shapeFlag&512?m.ctx.activate(f,h,y,C,x):xs(f,h,y,m,v,C,x):gn(c,f,x)},xs=(c,f,h,y,m,v,C)=>{const S=c.component=Xo(c,y,m);if(Mr(c)&&(S.ctx.renderer=yt),Qo(S,!1,C),S.asyncDep){if(m&&m.registerDep(S,se,C),!c.el){const x=S.subTree=Le(Ge);$(null,x,f,h),c.placeholder=x.el}}else se(S,c,f,h,m,v,C)},gn=(c,f,h)=>{const y=f.component=c.component;if(Fo(c,f,h))if(y.asyncDep&&!y.asyncResolved){q(y,f,h);return}else y.next=f,y.update();else f.el=c.el,y.vnode=f},se=(c,f,h,y,m,v,C)=>{const S=()=>{if(c.isMounted){let{next:T,bu:A,u:F,parent:L,vnode:B}=c;{const we=Gr(c);if(we){T&&(T.el=B.el,q(c,T,C)),we.asyncDep.then(()=>{c.isUnmounted||S()});return}}let H=T,le;Qe(c,!1),T?(T.el=B.el,q(c,T,C)):T=B,A&&Yt(A),(le=T.props&&T.props.onVnodeBeforeUpdate)&&Te(le,L,T,B),Qe(c,!0);const ce=Mn(c),Se=c.subTree;c.subTree=ce,g(Se,ce,p(Se.el),kt(Se),c,m,v),T.el=ce.el,H===null&&Ro(c,ce.el),F&&ae(F,m),(le=T.props&&T.props.onVnodeUpdated)&&ae(()=>Te(le,L,T,B),m)}else{let T;const{el:A,props:F}=f,{bm:L,m:B,parent:H,root:le,type:ce}=c,Se=It(f);Qe(c,!1),L&&Yt(L),!Se&&(T=F&&F.onVnodeBeforeMount)&&Te(T,H,f),Qe(c,!0);{le.ce&&le.ce._def.shadowRoot!==!1&&le.ce._injectChildStyle(ce);const we=c.subTree=Mn(c);g(null,we,h,y,c,m,v),f.el=we.el}if(B&&ae(B,m),!Se&&(T=F&&F.onVnodeMounted)){const we=f;ae(()=>Te(T,H,we),m)}(f.shapeFlag&256||H&&It(H.vnode)&&H.vnode.shapeFlag&256)&&c.a&&ae(c.a,m),c.isMounted=!0,f=h=y=null}};c.scope.on();const x=c.effect=new cr(S);c.scope.off();const b=c.update=x.run.bind(x),I=c.job=x.runIfDirty.bind(x);I.i=c,I.id=c.uid,x.scheduler=()=>ln(I),Qe(c,!0),b()},q=(c,f,h)=>{f.component=c;const y=c.vnode.props;c.vnode=f,c.next=null,Do(c,f.props,y,h),Ho(c,f.children,h),Ne(),Tn(c),je()},K=(c,f,h,y,m,v,C,S,x=!1)=>{const b=c&&c.children,I=c?c.shapeFlag:0,T=f.children,{patchFlag:A,shapeFlag:F}=f;if(A>0){if(A&128){Wt(b,T,h,y,m,v,C,S,x);return}else if(A&256){ze(b,T,h,y,m,v,C,S,x);return}}F&8?(I&16&&bt(b,m,v),T!==b&&a(h,T)):I&16?F&16?Wt(b,T,h,y,m,v,C,S,x):bt(b,m,v,!0):(I&8&&a(h,""),F&16&&Ke(T,h,y,m,v,C,S,x))},ze=(c,f,h,y,m,v,C,S,x)=>{c=c||ut,f=f||ut;const b=c.length,I=f.length,T=Math.min(b,I);let A;for(A=0;AI?bt(c,m,v,!0,!1,T):Ke(f,h,y,m,v,C,S,x,T)},Wt=(c,f,h,y,m,v,C,S,x)=>{let b=0;const I=f.length;let T=c.length-1,A=I-1;for(;b<=T&&b<=A;){const F=c[b],L=f[b]=x?ke(f[b]):Oe(f[b]);if(wt(F,L))g(F,L,h,null,m,v,C,S,x);else break;b++}for(;b<=T&&b<=A;){const F=c[T],L=f[A]=x?ke(f[A]):Oe(f[A]);if(wt(F,L))g(F,L,h,null,m,v,C,S,x);else break;T--,A--}if(b>T){if(b<=A){const F=A+1,L=FA)for(;b<=T;)xe(c[b],m,v,!0),b++;else{const F=b,L=b,B=new Map;for(b=L;b<=A;b++){const fe=f[b]=x?ke(f[b]):Oe(f[b]);fe.key!=null&&B.set(fe.key,b)}let H,le=0;const ce=A-L+1;let Se=!1,we=0;const xt=new Array(ce);for(b=0;b=ce){xe(fe,m,v,!0);continue}let Ce;if(fe.key!=null)Ce=B.get(fe.key);else for(H=L;H<=A;H++)if(xt[H-L]===0&&wt(fe,f[H])){Ce=H;break}Ce===void 0?xe(fe,m,v,!0):(xt[Ce-L]=b+1,Ce>=we?we=Ce:Se=!0,g(fe,f[Ce],h,null,m,v,C,S,x),le++)}const _n=Se?Bo(xt):ut;for(H=_n.length-1,b=ce-1;b>=0;b--){const fe=L+b,Ce=f[fe],bn=f[fe+1],yn=fe+1{const{el:v,type:C,transition:S,children:x,shapeFlag:b}=c;if(b&6){Xe(c.component.subTree,f,h,y);return}if(b&128){c.suspense.move(f,h,y);return}if(b&64){C.move(c,f,h,yt);return}if(C===de){n(v,f,h);for(let T=0;TS.enter(v),m);else{const{leave:T,delayLeave:A,afterLeave:F}=S,L=()=>{c.ctx.isUnmounted?r(v):n(v,f,h)},B=()=>{v._isLeaving&&v[io](!0),T(v,()=>{L(),F&&F()})};A?A(v,L,B):B()}else n(v,f,h)},xe=(c,f,h,y=!1,m=!1)=>{const{type:v,props:C,ref:S,children:x,dynamicChildren:b,shapeFlag:I,patchFlag:T,dirs:A,cacheIndex:F}=c;if(T===-2&&(m=!1),S!=null&&(Ne(),At(S,null,h,c,!0),je()),F!=null&&(f.renderCache[F]=void 0),I&256){f.ctx.deactivate(c);return}const L=I&1&&A,B=!It(c);let H;if(B&&(H=C&&C.onVnodeBeforeUnmount)&&Te(H,f,c),I&6)oi(c.component,h,y);else{if(I&128){c.suspense.unmount(h,y);return}L&&Ze(c,null,f,"beforeUnmount"),I&64?c.type.remove(c,f,h,yt,y):b&&!b.hasOnce&&(v!==de||T>0&&T&64)?bt(b,f,h,!1,!0):(v===de&&T&384||!m&&I&16)&&bt(x,f,h),y&&mn(c)}(B&&(H=C&&C.onVnodeUnmounted)||L)&&ae(()=>{H&&Te(H,f,c),L&&Ze(c,null,f,"unmounted")},h)},mn=c=>{const{type:f,el:h,anchor:y,transition:m}=c;if(f===de){ii(h,y);return}if(f===Ms){M(c);return}const v=()=>{r(h),m&&!m.persisted&&m.afterLeave&&m.afterLeave()};if(c.shapeFlag&1&&m&&!m.persisted){const{leave:C,delayLeave:S}=m,x=()=>C(h,v);S?S(c.el,v,x):x()}else v()},ii=(c,f)=>{let h;for(;c!==f;)h=w(c),r(c),c=h;r(f)},oi=(c,f,h)=>{const{bum:y,scope:m,job:v,subTree:C,um:S,m:x,a:b}=c;$n(x),$n(b),y&&Yt(y),m.stop(),v&&(v.flags|=8,xe(C,c,f,h)),S&&ae(S,f),ae(()=>{c.isUnmounted=!0},f)},bt=(c,f,h,y=!1,m=!1,v=0)=>{for(let C=v;C{if(c.shapeFlag&6)return kt(c.component.subTree);if(c.shapeFlag&128)return c.suspense.next();const f=w(c.anchor||c.el),h=f&&f[no];return h?w(h):f};let Ss=!1;const vn=(c,f,h)=>{let y;c==null?f._vnode&&(xe(f._vnode,null,null,!0),y=f._vnode.component):g(f._vnode||null,c,f,null,null,null,h),f._vnode=c,Ss||(Ss=!0,Tn(y),Tr(),Ss=!1)},yt={p:g,um:xe,m:Xe,r:mn,mt:xs,mc:Ke,pc:K,pbc:Ye,n:kt,o:e};return{render:vn,hydrate:void 0,createApp:Eo(vn)}}function Is({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Qe({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Ko(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Jr(e,t,s=!1){const n=e.children,r=t.children;if(R(n)&&R(r))for(let i=0;i>1,e[s[l]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,o=s[i-1];i-- >0;)s[i]=o,o=t[o];return s}function Gr(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Gr(t)}function $n(e){if(e)for(let t=0;te.__isSuspense;function Wo(e,t){t&&t.pendingBranch?R(e)?t.effects.push(...e):t.effects.push(e):Xi(e)}const de=Symbol.for("v-fgt"),vs=Symbol.for("v-txt"),Ge=Symbol.for("v-cmt"),Ms=Symbol.for("v-stc"),Ft=[];let he=null;function J(e=!1){Ft.push(he=e?null:[])}function ko(){Ft.pop(),he=Ft[Ft.length-1]||null}let Lt=1;function Dn(e,t=!1){Lt+=e,e<0&&he&&t&&(he.hasOnce=!0)}function Xr(e){return e.dynamicChildren=Lt>0?he||ut:null,ko(),Lt>0&&he&&he.push(e),e}function Q(e,t,s,n,r,i){return Xr(_(e,t,s,n,r,i,!0))}function Xt(e,t,s,n,r){return Xr(Le(e,t,s,n,r,!0))}function Zr(e){return e?e.__v_isVNode===!0:!1}function wt(e,t){return e.type===t.type&&e.key===t.key}const Qr=({key:e})=>e??null,Zt=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?X(e)||te(e)||D(e)?{i:pe,r:e,k:t,f:!!s}:e:null);function _(e,t=null,s=null,n=0,r=null,i=e===de?0:1,o=!1,l=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Qr(t),ref:t&&Zt(t),scopeId:Or,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:n,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:pe};return l?(an(u,s),i&128&&e.normalize(u)):s&&(u.shapeFlag|=X(s)?8:16),Lt>0&&!o&&he&&(u.patchFlag>0||i&6)&&u.patchFlag!==32&&he.push(u),u}const Le=qo;function qo(e,t=null,s=null,n=0,r=null,i=!1){if((!e||e===_o)&&(e=Ge),Zr(e)){const l=mt(e,t,!0);return s&&an(l,s),Lt>0&&!i&&he&&(l.shapeFlag&6?he[he.indexOf(e)]=l:he.push(l)),l.patchFlag=-2,l}if(nl(e)&&(e=e.__vccOpts),t){t=Jo(t);let{class:l,style:u}=t;l&&!X(l)&&(t.class=pt(l)),k(u)&&(on(u)&&!R(u)&&(u=oe({},u)),t.style=ds(u))}const o=X(e)?1:zr(e)?128:ro(e)?64:k(e)?4:D(e)?2:0;return _(e,t,s,n,r,o,i,!0)}function Jo(e){return e?on(e)||Ur(e)?oe({},e):e:null}function mt(e,t,s=!1,n=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:u}=e,d=t?Go(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&Qr(d),ref:t&&t.ref?s&&i?R(i)?i.concat(Zt(t)):[i,Zt(t)]:Zt(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==de?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&mt(e.ssContent),ssFallback:e.ssFallback&&mt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&n&&cn(a,u.clone(a)),a}function Ws(e=" ",t=0){return Le(vs,null,e,t)}function ge(e="",t=!1){return t?(J(),Xt(Ge,null,e)):Le(Ge,null,e)}function Oe(e){return e==null||typeof e=="boolean"?Le(Ge):R(e)?Le(de,null,e.slice()):Zr(e)?ke(e):Le(vs,null,String(e))}function ke(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:mt(e)}function an(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(R(t))s=16;else if(typeof t=="object")if(n&65){const r=t.default;r&&(r._c&&(r._d=!1),an(e,r()),r._c&&(r._d=!0));return}else{s=32;const r=t._;!r&&!Ur(t)?t._ctx=pe:r===3&&pe&&(pe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else D(t)?(t={default:t,_ctx:pe},s=32):(t=String(t),n&64?(s=16,t=[Ws(t)]):s=8);e.children=t,e.shapeFlag|=s}function Go(...e){const t={};for(let s=0;sie||pe;let os,ks;{const e=as(),t=(s,n)=>{let r;return(r=e[s])||(r=e[s]=[]),r.push(n),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};os=t("__VUE_INSTANCE_SETTERS__",s=>ie=s),ks=t("__VUE_SSR_SETTERS__",s=>Nt=s)}const Vt=e=>{const t=ie;return os(e),e.scope.on(),()=>{e.scope.off(),os(t)}},Ln=()=>{ie&&ie.scope.off(),os(null)};function ei(e){return e.vnode.shapeFlag&4}let Nt=!1;function Qo(e,t=!1,s=!1){t&&ks(t);const{props:n,children:r}=e.vnode,i=ei(e);$o(e,n,i,t),jo(e,r,s||t);const o=i?el(e,t):void 0;return t&&ks(!1),o}function el(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,bo);const{setup:n}=s;if(n){Ne();const r=e.setupContext=n.length>1?sl(e):null,i=Vt(e),o=Ht(n,e,0,[e.props,r]),l=er(o);if(je(),i(),(l||e.sp)&&!It(e)&&Ir(e),l){if(o.then(Ln,Ln),t)return o.then(u=>{Nn(e,u)}).catch(u=>{ps(u,e,0)});e.asyncDep=o}else Nn(e,o)}else ti(e)}function Nn(e,t,s){D(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:k(t)&&(e.setupState=Sr(t)),ti(e)}function ti(e,t,s){const n=e.type;e.render||(e.render=n.render||Ae);{const r=Vt(e);Ne();try{yo(e)}finally{je(),r()}}}const tl={get(e,t){return ee(e,"get",""),e[t]}};function sl(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,tl),slots:e.slots,emit:e.emit,expose:t}}function _s(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Sr(Hi(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Mt)return Mt[s](e)},has(t,s){return s in t||s in Mt}})):e.proxy}function nl(e){return D(e)&&"__vccOpts"in e}const ct=(e,t)=>ki(e,t,Nt),rl="3.5.26";/** +* @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-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 new file mode 100644 index 0000000..bfabf9b --- /dev/null +++ b/wails/assets/index.html @@ -0,0 +1,30 @@ + + + + + + 视频拼接工具 + + + + + +
+ + + diff --git a/wails/build/Taskfile.yml b/wails/build/Taskfile.yml new file mode 100644 index 0000000..d4d3bb6 --- /dev/null +++ b/wails/build/Taskfile.yml @@ -0,0 +1,193 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (DEV={{.DEV}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + vars: + BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + + + frontend:vendor:puppertino: + summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling + sources: + - frontend/public/puppertino/puppertino.css + generates: + - frontend/public/puppertino/puppertino.css + cmds: + - | + set -euo pipefail + mkdir -p frontend/public/puppertino + # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error. + if [ ! -f frontend/public/puppertino/puppertino.css ]; then + echo "No bundled Puppertino found. Attempting to fetch from GitHub..." + if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then + curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true + echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css" + else + echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it." + fi + else + echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css" + fi + # Ensure index.html includes Puppertino CSS and button classes + INDEX_HTML=frontend/index.html + if [ -f "$INDEX_HTML" ]; then + if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then + # Insert Puppertino link tag after style.css link + awk ' + /href="\/style.css"\/?/ && !x { print; print " "; x=1; next }1 + ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML" + fi + # Replace default .btn with Puppertino primary button classes if present + sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true + fi + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + desc: | + Builds the Docker image needed for cross-compiling to any platform. + Run this once to enable cross-platform builds from any OS. + cmds: + - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/ + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + + ios:device:list: + summary: Lists connected iOS devices (UDIDs) + cmds: + - xcrun xcdevice list + + ios:run:device: + summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl) + vars: + PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/.xcodeproj + SCHEME: '{{.SCHEME}}' # e.g., ios.dev + CONFIG: '{{.CONFIG | default "Debug"}}' + DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}' + UDID: '{{.UDID}}' # from `task ios:device:list` + BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev + TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing + preconditions: + - sh: xcrun -f xcodebuild + msg: "xcodebuild not found. Please install Xcode." + - sh: xcrun -f devicectl + msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)." + - sh: test -n '{{.PROJECT}}' + msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)." + - sh: test -n '{{.SCHEME}}' + msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)." + - sh: test -n '{{.UDID}}' + msg: "Set UDID to your device UDID (see: task ios:device:list)." + - sh: test -n '{{.BUNDLE_ID}}' + msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)." + cmds: + - | + set -euo pipefail + echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}" + XCB_ARGS=( + -project "{{.PROJECT}}" + -scheme "{{.SCHEME}}" + -configuration "{{.CONFIG}}" + -destination "id={{.UDID}}" + -derivedDataPath "{{.DERIVED}}" + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + # Optionally inject signing identifiers if provided + if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi + if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi + xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true + # If xcpretty isn't installed, run without it + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + xcodebuild "${XCB_ARGS[@]}" build + fi + # Find built .app + APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1) + if [ -z "$APP_PATH" ]; then + echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2 + exit 1 + fi + echo "Installing: $APP_PATH" + xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH" + echo "Launching: {{.BUNDLE_ID}}" + xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}" diff --git a/wails/build/android/Taskfile.yml b/wails/build/android/Taskfile.yml new file mode 100644 index 0000000..aca62e4 --- /dev/null +++ b/wails/build/android/Taskfile.yml @@ -0,0 +1,237 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + APP_ID: '{{.APP_ID | default "com.wails.app"}}' + MIN_SDK: '21' + TARGET_SDK: '34' + NDK_VERSION: 'r26d' + +tasks: + install:deps: + summary: Check and install Android development dependencies + cmds: + - go run build/android/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}' + prompt: This will check and install Android development dependencies. Continue? + + build: + summary: Creates a build of the application for Android + deps: + - task: common:go:mod:tidy + - task: generate:android:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building Android app {{.APP_NAME}}..." + - task: compile:go:shared + vars: + ARCH: '{{.ARCH | default "arm64"}}' + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + compile:go:shared: + summary: Compile Go code to shared library (.so) + cmds: + - | + NDK_ROOT="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/{{.NDK_VERSION}}}" + if [ ! -d "$NDK_ROOT" ]; then + echo "Error: Android NDK not found at $NDK_ROOT" + echo "Please set ANDROID_NDK_HOME or install NDK {{.NDK_VERSION}} via Android Studio" + exit 1 + fi + + # Determine toolchain based on host OS + case "$(uname -s)" in + Darwin) HOST_TAG="darwin-x86_64" ;; + Linux) HOST_TAG="linux-x86_64" ;; + *) echo "Unsupported host OS"; exit 1 ;; + esac + + TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG" + + # Set compiler based on architecture + case "{{.ARCH}}" in + arm64) + export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=arm64 + JNI_DIR="arm64-v8a" + ;; + amd64|x86_64) + export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=amd64 + JNI_DIR="x86_64" + ;; + *) + echo "Unsupported architecture: {{.ARCH}}" + exit 1 + ;; + esac + + export CGO_ENABLED=1 + export GOOS=android + + mkdir -p {{.BIN_DIR}} + mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR + + go build -buildmode=c-shared {{.BUILD_FLAGS}} \ + -o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + + compile:go:all-archs: + summary: Compile Go code for all Android architectures (fat APK) + cmds: + - task: compile:go:shared + vars: + ARCH: arm64 + - task: compile:go:shared + vars: + ARCH: amd64 + + package: + summary: Packages a production build of the application into an APK + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: assemble:apk + + package:fat: + summary: Packages a production build for all architectures (fat APK) + cmds: + - task: compile:go:all-archs + - task: assemble:apk + + assemble:apk: + summary: Assembles the APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleDebug + cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk" + echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk" + + assemble:apk:release: + summary: Assembles a release APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleRelease + cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + + generate:android:bindings: + internal: true + summary: Generates bindings for Android + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: android + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + + ensure-emulator: + internal: true + summary: Ensure Android Emulator is running + silent: true + cmds: + - | + # Check if an emulator is already running + if adb devices | grep -q "emulator"; then + echo "Emulator already running" + exit 0 + fi + + # Get first available AVD + AVD_NAME=$(emulator -list-avds | head -1) + if [ -z "$AVD_NAME" ]; then + echo "No Android Virtual Devices found." + echo "Create one using: Android Studio > Tools > Device Manager" + exit 1 + fi + + echo "Starting emulator: $AVD_NAME" + emulator -avd "$AVD_NAME" -no-snapshot-load & + + # Wait for emulator to boot (max 60 seconds) + echo "Waiting for emulator to boot..." + adb wait-for-device + + for i in {1..60}; do + BOOT_COMPLETED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$BOOT_COMPLETED" = "1" ]; then + echo "Emulator booted successfully" + exit 0 + fi + sleep 1 + done + + echo "Emulator boot timeout" + exit 1 + preconditions: + - sh: command -v adb + msg: "adb not found. Please install Android SDK and add platform-tools to PATH" + - sh: command -v emulator + msg: "emulator not found. Please install Android SDK and add emulator to PATH" + + deploy-emulator: + summary: Deploy to Android Emulator + deps: [package] + cmds: + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + run: + summary: Run the application in Android Emulator + deps: + - task: ensure-emulator + - task: build + vars: + ARCH: x86_64 + cmds: + - task: assemble:apk + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + logs: + summary: Stream Android logcat filtered to this app + cmds: + - adb logcat -v time | grep -E "(Wails|{{.APP_NAME}})" + + logs:all: + summary: Stream all Android logcat (verbose) + cmds: + - adb logcat -v time + + clean: + summary: Clean build artifacts + cmds: + - rm -rf {{.BIN_DIR}} + - rm -rf build/android/app/build + - rm -rf build/android/app/src/main/jniLibs/*/libwails.so + - rm -rf build/android/.gradle diff --git a/wails/build/android/app/build.gradle b/wails/build/android/app/build.gradle new file mode 100644 index 0000000..78fdbf7 --- /dev/null +++ b/wails/build/android/app/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.wails.app' + compileSdk 34 + + buildFeatures { + buildConfig = true + } + + defaultConfig { + applicationId "com.wails.app" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + // Configure supported ABIs + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + debuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + // Source sets configuration + sourceSets { + main { + // JNI libraries are in jniLibs folder + jniLibs.srcDirs = ['src/main/jniLibs'] + // Assets for the WebView + assets.srcDirs = ['src/main/assets'] + } + } + + // Packaging options + packagingOptions { + // Don't strip Go symbols in debug builds + doNotStrip '*/arm64-v8a/libwails.so' + doNotStrip '*/x86_64/libwails.so' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.webkit:webkit:1.9.0' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/wails/build/android/app/proguard-rules.pro b/wails/build/android/app/proguard-rules.pro new file mode 100644 index 0000000..8b88c3d --- /dev/null +++ b/wails/build/android/app/proguard-rules.pro @@ -0,0 +1,12 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Wails bridge classes +-keep class com.wails.app.WailsBridge { *; } +-keep class com.wails.app.WailsJSBridge { *; } diff --git a/wails/build/android/app/src/main/AndroidManifest.xml b/wails/build/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6c7982a --- /dev/null +++ b/wails/build/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/wails/build/android/app/src/main/java/com/wails/app/MainActivity.java b/wails/build/android/app/src/main/java/com/wails/app/MainActivity.java new file mode 100644 index 0000000..3067fee --- /dev/null +++ b/wails/build/android/app/src/main/java/com/wails/app/MainActivity.java @@ -0,0 +1,198 @@ +package com.wails.app; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.webkit.WebViewAssetLoader; +import com.wails.app.BuildConfig; + +/** + * MainActivity hosts the WebView and manages the Wails application lifecycle. + * It uses WebViewAssetLoader to serve assets from the Go library without + * requiring a network server. + */ +public class MainActivity extends AppCompatActivity { + private static final String TAG = "WailsActivity"; + private static final String WAILS_SCHEME = "https"; + private static final String WAILS_HOST = "wails.localhost"; + + private WebView webView; + private WailsBridge bridge; + private WebViewAssetLoader assetLoader; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Initialize the native Go library + bridge = new WailsBridge(this); + bridge.initialize(); + + // Set up WebView + setupWebView(); + + // Load the application + loadApplication(); + } + + @SuppressLint("SetJavaScriptEnabled") + private void setupWebView() { + webView = findViewById(R.id.webview); + + // Configure WebView settings + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); + settings.setMediaPlaybackRequiresUserGesture(false); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + + // Enable debugging in debug builds + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true); + } + + // Set up asset loader for serving local assets + assetLoader = new WebViewAssetLoader.Builder() + .setDomain(WAILS_HOST) + .addPathHandler("/", new WailsPathHandler(bridge)) + .build(); + + // Set up WebView client to intercept requests + webView.setWebViewClient(new WebViewClient() { + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); + Log.d(TAG, "Intercepting request: " + url); + + // Handle wails.localhost requests + if (request.getUrl().getHost() != null && + request.getUrl().getHost().equals(WAILS_HOST)) { + + // For wails API calls (runtime, capabilities, etc.), we need to pass the full URL + // including query string because WebViewAssetLoader.PathHandler strips query params + String path = request.getUrl().getPath(); + if (path != null && path.startsWith("/wails/")) { + // Get full path with query string for runtime calls + String fullPath = path; + String query = request.getUrl().getQuery(); + if (query != null && !query.isEmpty()) { + fullPath = path + "?" + query; + } + Log.d(TAG, "Wails API call detected, full path: " + fullPath); + + // Call bridge directly with full path + byte[] data = bridge.serveAsset(fullPath, request.getMethod(), "{}"); + if (data != null && data.length > 0) { + java.io.InputStream inputStream = new java.io.ByteArrayInputStream(data); + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + headers.put("Content-Type", "application/json"); + + return new WebResourceResponse( + "application/json", + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + // Return error response if data is null + return new WebResourceResponse( + "application/json", + "UTF-8", + 500, + "Internal Error", + new java.util.HashMap<>(), + new java.io.ByteArrayInputStream("{}".getBytes()) + ); + } + + // For regular assets, use the asset loader + return assetLoader.shouldInterceptRequest(request.getUrl()); + } + + return super.shouldInterceptRequest(view, request); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page loaded: " + url); + // Inject Wails runtime + bridge.injectRuntime(webView, url); + } + }); + + // Add JavaScript interface for Go communication + webView.addJavascriptInterface(new WailsJSBridge(bridge, webView), "wails"); + } + + private void loadApplication() { + // Load the main page from the asset server + String url = WAILS_SCHEME + "://" + WAILS_HOST + "/"; + Log.d(TAG, "Loading URL: " + url); + webView.loadUrl(url); + } + + /** + * Execute JavaScript in the WebView from the Go side + */ + public void executeJavaScript(final String js) { + runOnUiThread(() -> { + if (webView != null) { + webView.evaluateJavascript(js, null); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + if (bridge != null) { + bridge.onResume(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (bridge != null) { + bridge.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (bridge != null) { + bridge.shutdown(); + } + if (webView != null) { + webView.destroy(); + } + } + + @Override + public void onBackPressed() { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } +} diff --git a/wails/build/android/app/src/main/java/com/wails/app/WailsBridge.java b/wails/build/android/app/src/main/java/com/wails/app/WailsBridge.java new file mode 100644 index 0000000..3dab652 --- /dev/null +++ b/wails/build/android/app/src/main/java/com/wails/app/WailsBridge.java @@ -0,0 +1,214 @@ +package com.wails.app; + +import android.content.Context; +import android.util.Log; +import android.webkit.WebView; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * WailsBridge manages the connection between the Java/Android side and the Go native library. + * It handles: + * - Loading and initializing the native Go library + * - Serving asset requests from Go + * - Passing messages between JavaScript and Go + * - Managing callbacks for async operations + */ +public class WailsBridge { + private static final String TAG = "WailsBridge"; + + static { + // Load the native Go library + System.loadLibrary("wails"); + } + + private final Context context; + private final AtomicInteger callbackIdGenerator = new AtomicInteger(0); + private final ConcurrentHashMap pendingAssetCallbacks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pendingMessageCallbacks = new ConcurrentHashMap<>(); + private WebView webView; + private volatile boolean initialized = false; + + // Native methods - implemented in Go + private static native void nativeInit(WailsBridge bridge); + private static native void nativeShutdown(); + private static native void nativeOnResume(); + private static native void nativeOnPause(); + private static native void nativeOnPageFinished(String url); + private static native byte[] nativeServeAsset(String path, String method, String headers); + private static native String nativeHandleMessage(String message); + private static native String nativeGetAssetMimeType(String path); + + public WailsBridge(Context context) { + this.context = context; + } + + /** + * Initialize the native Go library + */ + public void initialize() { + if (initialized) { + return; + } + + Log.i(TAG, "Initializing Wails bridge..."); + try { + nativeInit(this); + initialized = true; + Log.i(TAG, "Wails bridge initialized successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to initialize Wails bridge", e); + } + } + + /** + * Shutdown the native Go library + */ + public void shutdown() { + if (!initialized) { + return; + } + + Log.i(TAG, "Shutting down Wails bridge..."); + try { + nativeShutdown(); + initialized = false; + } catch (Exception e) { + Log.e(TAG, "Error during shutdown", e); + } + } + + /** + * Called when the activity resumes + */ + public void onResume() { + if (initialized) { + nativeOnResume(); + } + } + + /** + * Called when the activity pauses + */ + public void onPause() { + if (initialized) { + nativeOnPause(); + } + } + + /** + * Serve an asset from the Go asset server + * @param path The URL path requested + * @param method The HTTP method + * @param headers The request headers as JSON + * @return The asset data, or null if not found + */ + public byte[] serveAsset(String path, String method, String headers) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot serve asset: " + path); + return null; + } + + Log.d(TAG, "Serving asset: " + path); + try { + return nativeServeAsset(path, method, headers); + } catch (Exception e) { + Log.e(TAG, "Error serving asset: " + path, e); + return null; + } + } + + /** + * Get the MIME type for an asset + * @param path The asset path + * @return The MIME type string + */ + public String getAssetMimeType(String path) { + if (!initialized) { + return "application/octet-stream"; + } + + try { + String mimeType = nativeGetAssetMimeType(path); + return mimeType != null ? mimeType : "application/octet-stream"; + } catch (Exception e) { + Log.e(TAG, "Error getting MIME type for: " + path, e); + return "application/octet-stream"; + } + } + + /** + * Handle a message from JavaScript + * @param message The message from JavaScript (JSON) + * @return The response to send back to JavaScript (JSON) + */ + public String handleMessage(String message) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot handle message"); + return "{\"error\":\"Bridge not initialized\"}"; + } + + Log.d(TAG, "Handling message from JS: " + message); + try { + return nativeHandleMessage(message); + } catch (Exception e) { + Log.e(TAG, "Error handling message", e); + return "{\"error\":\"" + e.getMessage() + "\"}"; + } + } + + /** + * Inject the Wails runtime JavaScript into the WebView. + * Called when the page finishes loading. + * @param webView The WebView to inject into + * @param url The URL that finished loading + */ + public void injectRuntime(WebView webView, String url) { + this.webView = webView; + // Notify Go side that page has finished loading so it can inject the runtime + Log.d(TAG, "Page finished loading: " + url + ", notifying Go side"); + if (initialized) { + nativeOnPageFinished(url); + } + } + + /** + * Execute JavaScript in the WebView (called from Go side) + * @param js The JavaScript code to execute + */ + public void executeJavaScript(String js) { + if (webView != null) { + webView.post(() -> webView.evaluateJavascript(js, null)); + } + } + + /** + * Called from Go when an event needs to be emitted to JavaScript + * @param eventName The event name + * @param eventData The event data (JSON) + */ + public void emitEvent(String eventName, String eventData) { + String js = String.format("window.wails && window.wails._emit('%s', %s);", + escapeJsString(eventName), eventData); + executeJavaScript(js); + } + + private String escapeJsString(String str) { + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + + // Callback interfaces + public interface AssetCallback { + void onAssetReady(byte[] data, String mimeType); + void onAssetError(String error); + } + + public interface MessageCallback { + void onResponse(String response); + void onError(String error); + } +} diff --git a/wails/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java b/wails/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java new file mode 100644 index 0000000..98ae5b2 --- /dev/null +++ b/wails/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java @@ -0,0 +1,142 @@ +package com.wails.app; + +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import com.wails.app.BuildConfig; + +/** + * WailsJSBridge provides the JavaScript interface that allows the web frontend + * to communicate with the Go backend. This is exposed to JavaScript as the + * `window.wails` object. + * + * Similar to iOS's WKScriptMessageHandler but using Android's addJavascriptInterface. + */ +public class WailsJSBridge { + private static final String TAG = "WailsJSBridge"; + + private final WailsBridge bridge; + private final WebView webView; + + public WailsJSBridge(WailsBridge bridge, WebView webView) { + this.bridge = bridge; + this.webView = webView; + } + + /** + * Send a message to Go and return the response synchronously. + * Called from JavaScript: wails.invoke(message) + * + * @param message The message to send (JSON string) + * @return The response from Go (JSON string) + */ + @JavascriptInterface + public String invoke(String message) { + Log.d(TAG, "Invoke called: " + message); + return bridge.handleMessage(message); + } + + /** + * Send a message to Go asynchronously. + * The response will be sent back via a callback. + * Called from JavaScript: wails.invokeAsync(callbackId, message) + * + * @param callbackId The callback ID to use for the response + * @param message The message to send (JSON string) + */ + @JavascriptInterface + public void invokeAsync(final String callbackId, final String message) { + Log.d(TAG, "InvokeAsync called: " + message); + + // Handle in background thread to not block JavaScript + new Thread(() -> { + try { + String response = bridge.handleMessage(message); + sendCallback(callbackId, response, null); + } catch (Exception e) { + Log.e(TAG, "Error in async invoke", e); + sendCallback(callbackId, null, e.getMessage()); + } + }).start(); + } + + /** + * Log a message from JavaScript to Android's logcat + * Called from JavaScript: wails.log(level, message) + * + * @param level The log level (debug, info, warn, error) + * @param message The message to log + */ + @JavascriptInterface + public void log(String level, String message) { + switch (level.toLowerCase()) { + case "debug": + Log.d(TAG + "/JS", message); + break; + case "info": + Log.i(TAG + "/JS", message); + break; + case "warn": + Log.w(TAG + "/JS", message); + break; + case "error": + Log.e(TAG + "/JS", message); + break; + default: + Log.v(TAG + "/JS", message); + break; + } + } + + /** + * Get the platform name + * Called from JavaScript: wails.platform() + * + * @return "android" + */ + @JavascriptInterface + public String platform() { + return "android"; + } + + /** + * Check if we're running in debug mode + * Called from JavaScript: wails.isDebug() + * + * @return true if debug build, false otherwise + */ + @JavascriptInterface + public boolean isDebug() { + return BuildConfig.DEBUG; + } + + /** + * Send a callback response to JavaScript + */ + private void sendCallback(String callbackId, String result, String error) { + final String js; + if (error != null) { + js = String.format( + "window.wails && window.wails._callback('%s', null, '%s');", + escapeJsString(callbackId), + escapeJsString(error) + ); + } else { + js = String.format( + "window.wails && window.wails._callback('%s', %s, null);", + escapeJsString(callbackId), + result != null ? result : "null" + ); + } + + webView.post(() -> webView.evaluateJavascript(js, null)); + } + + private String escapeJsString(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } +} diff --git a/wails/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java b/wails/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java new file mode 100644 index 0000000..326fa9b --- /dev/null +++ b/wails/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java @@ -0,0 +1,118 @@ +package com.wails.app; + +import android.net.Uri; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewAssetLoader; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * WailsPathHandler implements WebViewAssetLoader.PathHandler to serve assets + * from the Go asset server. This allows the WebView to load assets without + * using a network server, similar to iOS's WKURLSchemeHandler. + */ +public class WailsPathHandler implements WebViewAssetLoader.PathHandler { + private static final String TAG = "WailsPathHandler"; + + private final WailsBridge bridge; + + public WailsPathHandler(WailsBridge bridge) { + this.bridge = bridge; + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + Log.d(TAG, "Handling path: " + path); + + // Normalize path + if (path.isEmpty() || path.equals("/")) { + path = "/index.html"; + } + + // Get asset from Go + byte[] data = bridge.serveAsset(path, "GET", "{}"); + + if (data == null || data.length == 0) { + Log.w(TAG, "Asset not found: " + path); + return null; // Return null to let WebView handle 404 + } + + // Determine MIME type + String mimeType = bridge.getAssetMimeType(path); + Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)"); + + // Create response + InputStream inputStream = new ByteArrayInputStream(data); + Map headers = new HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + + return new WebResourceResponse( + mimeType, + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + + /** + * Determine MIME type from file extension + */ + private String getMimeType(String path) { + String lowerPath = path.toLowerCase(); + + if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) { + return "text/html"; + } else if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) { + return "application/javascript"; + } else if (lowerPath.endsWith(".css")) { + return "text/css"; + } else if (lowerPath.endsWith(".json")) { + return "application/json"; + } else if (lowerPath.endsWith(".png")) { + return "image/png"; + } else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (lowerPath.endsWith(".gif")) { + return "image/gif"; + } else if (lowerPath.endsWith(".svg")) { + return "image/svg+xml"; + } else if (lowerPath.endsWith(".ico")) { + return "image/x-icon"; + } else if (lowerPath.endsWith(".woff")) { + return "font/woff"; + } else if (lowerPath.endsWith(".woff2")) { + return "font/woff2"; + } else if (lowerPath.endsWith(".ttf")) { + return "font/ttf"; + } else if (lowerPath.endsWith(".eot")) { + return "application/vnd.ms-fontobject"; + } else if (lowerPath.endsWith(".xml")) { + return "application/xml"; + } else if (lowerPath.endsWith(".txt")) { + return "text/plain"; + } else if (lowerPath.endsWith(".wasm")) { + return "application/wasm"; + } else if (lowerPath.endsWith(".mp3")) { + return "audio/mpeg"; + } else if (lowerPath.endsWith(".mp4")) { + return "video/mp4"; + } else if (lowerPath.endsWith(".webm")) { + return "video/webm"; + } else if (lowerPath.endsWith(".webp")) { + return "image/webp"; + } + + return "application/octet-stream"; + } +} diff --git a/wails/build/android/app/src/main/res/layout/activity_main.xml b/wails/build/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f278384 --- /dev/null +++ b/wails/build/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/wails/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/wails/build/android/app/src/main/res/values/colors.xml b/wails/build/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..dd33f3b --- /dev/null +++ b/wails/build/android/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #3574D4 + #2C5FB8 + #1B2636 + #FFFFFFFF + #FF000000 + diff --git a/wails/build/android/app/src/main/res/values/strings.xml b/wails/build/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3ed9e47 --- /dev/null +++ b/wails/build/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Wails App + diff --git a/wails/build/android/app/src/main/res/values/themes.xml b/wails/build/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..be8a282 --- /dev/null +++ b/wails/build/android/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/wails/build/android/build.gradle b/wails/build/android/build.gradle new file mode 100644 index 0000000..d7fbab3 --- /dev/null +++ b/wails/build/android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/wails/build/android/gradle.properties b/wails/build/android/gradle.properties new file mode 100644 index 0000000..b9d4426 --- /dev/null +++ b/wails/build/android/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/build/optimize-your-build#parallel +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/wails/build/android/gradle/wrapper/gradle-wrapper.jar b/wails/build/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/wails/build/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/wails/build/android/gradle/wrapper/gradle-wrapper.properties b/wails/build/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/wails/build/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wails/build/android/gradlew b/wails/build/android/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/wails/build/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wails/build/android/gradlew.bat b/wails/build/android/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/wails/build/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wails/build/android/main_android.go b/wails/build/android/main_android.go new file mode 100644 index 0000000..70a7164 --- /dev/null +++ b/wails/build/android/main_android.go @@ -0,0 +1,11 @@ +//go:build android + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +func init() { + // Register main function to be called when the Android app initializes + // This is necessary because in c-shared build mode, main() is not automatically called + application.RegisterAndroidMain(main) +} diff --git a/wails/build/android/scripts/deps/install_deps.go b/wails/build/android/scripts/deps/install_deps.go new file mode 100644 index 0000000..d9dfedf --- /dev/null +++ b/wails/build/android/scripts/deps/install_deps.go @@ -0,0 +1,151 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + fmt.Println("Checking Android development dependencies...") + fmt.Println() + + errors := []string{} + + // Check Go + if !checkCommand("go", "version") { + errors = append(errors, "Go is not installed. Install from https://go.dev/dl/") + } else { + fmt.Println("✓ Go is installed") + } + + // Check ANDROID_HOME + androidHome := os.Getenv("ANDROID_HOME") + if androidHome == "" { + androidHome = os.Getenv("ANDROID_SDK_ROOT") + } + if androidHome == "" { + // Try common default locations + home, _ := os.UserHomeDir() + possiblePaths := []string{ + filepath.Join(home, "Android", "Sdk"), + filepath.Join(home, "Library", "Android", "sdk"), + "/usr/local/share/android-sdk", + } + for _, p := range possiblePaths { + if _, err := os.Stat(p); err == nil { + androidHome = p + break + } + } + } + + if androidHome == "" { + errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable") + } else { + fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome) + } + + // Check adb + if !checkCommand("adb", "version") { + if androidHome != "" { + platformTools := filepath.Join(androidHome, "platform-tools") + errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools)) + } else { + errors = append(errors, "adb not found. Install Android SDK Platform-Tools") + } + } else { + fmt.Println("✓ adb is installed") + } + + // Check emulator + if !checkCommand("emulator", "-list-avds") { + if androidHome != "" { + emulatorPath := filepath.Join(androidHome, "emulator") + errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath)) + } else { + errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager") + } + } else { + fmt.Println("✓ Android Emulator is installed") + } + + // Check NDK + ndkHome := os.Getenv("ANDROID_NDK_HOME") + if ndkHome == "" && androidHome != "" { + // Look for NDK in default location + ndkDir := filepath.Join(androidHome, "ndk") + if entries, err := os.ReadDir(ndkDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + ndkHome = filepath.Join(ndkDir, entry.Name()) + break + } + } + } + } + + if ndkHome == "" { + errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)") + } else { + fmt.Printf("✓ Android NDK: %s\n", ndkHome) + } + + // Check Java + if !checkCommand("java", "-version") { + errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)") + } else { + fmt.Println("✓ Java is installed") + } + + // Check for AVD (Android Virtual Device) + if checkCommand("emulator", "-list-avds") { + cmd := exec.Command("emulator", "-list-avds") + output, err := cmd.Output() + if err == nil && len(strings.TrimSpace(string(output))) > 0 { + avds := strings.Split(strings.TrimSpace(string(output)), "\n") + fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds)) + } else { + fmt.Println("⚠ No Android Virtual Devices found. Create one via Android Studio > Tools > Device Manager") + } + } + + fmt.Println() + + if len(errors) > 0 { + fmt.Println("❌ Missing dependencies:") + for _, err := range errors { + fmt.Printf(" - %s\n", err) + } + fmt.Println() + fmt.Println("Setup instructions:") + fmt.Println("1. Install Android Studio: https://developer.android.com/studio") + fmt.Println("2. Open SDK Manager and install:") + fmt.Println(" - Android SDK Platform (API 34)") + fmt.Println(" - Android SDK Build-Tools") + fmt.Println(" - Android SDK Platform-Tools") + fmt.Println(" - Android Emulator") + fmt.Println(" - NDK (Side by side)") + fmt.Println("3. Set environment variables:") + if runtime.GOOS == "darwin" { + fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk") + } else { + fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk") + } + fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator") + fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager") + os.Exit(1) + } + + fmt.Println("✓ All Android development dependencies are installed!") +} + +func checkCommand(name string, args ...string) bool { + cmd := exec.Command(name, args...) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} diff --git a/wails/build/android/settings.gradle b/wails/build/android/settings.gradle new file mode 100644 index 0000000..a3f3ec3 --- /dev/null +++ b/wails/build/android/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "WailsApp" +include ':app' diff --git a/wails/build/appicon.png b/wails/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/wails/build/appicon.png differ diff --git a/wails/build/config.yml b/wails/build/config.yml new file mode 100644 index 0000000..a9e17c8 --- /dev/null +++ b/wails/build/config.yml @@ -0,0 +1,78 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + +# iOS build configuration (uncomment to customise iOS project generation) +# Note: Keys under `ios` OVERRIDE values under `info` when set. +# ios: +# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier) +# bundleID: "com.mycompany.myproduct" +# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) +# displayName: "My Product" +# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) +# version: "0.0.1" +# # The company/organisation name for templates and project settings +# company: "My Company" +# # Additional comments to embed in Info.plist metadata +# comments: "Some Product Comments" + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + - assets + - Log + file: + - .DS_Store + - .gitignore + - .gitkeep + - "*.log" + watched_extension: + - "*.go" + - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive. + - "*.ts" # The frontend directory will be excluded entirely by the setting above. + git_ignore: true + executes: + - cmd: wails3 build DEV=true + type: blocking + - cmd: wails3 task common:dev:frontend + type: background + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/wails/build/darwin/Info.dev.plist b/wails/build/darwin/Info.dev.plist new file mode 100644 index 0000000..5419a80 --- /dev/null +++ b/wails/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + videoconcat + CFBundleIdentifier + com.example.videoconcat + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © 2026, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/wails/build/darwin/Info.plist b/wails/build/darwin/Info.plist new file mode 100644 index 0000000..41e048d --- /dev/null +++ b/wails/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + videoconcat + CFBundleIdentifier + com.example.videoconcat + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © 2026, My Company + + \ No newline at end of file diff --git a/wails/build/darwin/Taskfile.yml b/wails/build/darwin/Taskfile.yml new file mode 100644 index 0000000..50600ce --- /dev/null +++ b/wails/build/darwin/Taskfile.yml @@ -0,0 +1,199 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)" + # KEYCHAIN_PROFILE: "my-notarize-profile" + # ENTITLEMENTS: "build/darwin/entitlements.plist" + + # Docker image for cross-compilation (used when building on non-macOS) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application + cmds: + - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + + build:native: + summary: Builds the application natively on macOS + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + + build:docker: + summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + # Handles both relative (=> ../) and absolute (=> /) paths + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}' + + build:universal:lipo:native: + summary: Creates universal binary using native lipo (macOS) + internal: true + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + build:universal:lipo:go: + summary: Creates universal binary using wails3 tool lipo (Linux/Windows) + internal: true + cmds: + - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages the application into a `.app` bundle + deps: + - task: build + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" + - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' + + codesign:adhoc: + summary: Ad-hoc signs the app bundle (macOS only) + internal: true + cmds: + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + codesign:skip: + summary: Skips codesigning when cross-compiling + internal: true + cmds: + - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."' + + run: + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist" + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' + + sign: + summary: Signs the application bundle with Developer ID + desc: | + Signs the .app bundle for distribution. + Configure SIGN_IDENTITY in the vars section at the top of this file. + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + + sign:notarize: + summary: Signs and notarizes the application bundle + desc: | + Signs the .app bundle and submits it for notarization. + Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file. + + Setup (one-time): + wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile" + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + - sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]' + msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" diff --git a/wails/build/darwin/icons.icns b/wails/build/darwin/icons.icns new file mode 100644 index 0000000..1b5bd4c Binary files /dev/null and b/wails/build/darwin/icons.icns differ diff --git a/wails/build/docker/Dockerfile.cross b/wails/build/docker/Dockerfile.cross new file mode 100644 index 0000000..474c055 --- /dev/null +++ b/wails/build/docker/Dockerfile.cross @@ -0,0 +1,195 @@ +# Cross-compile Wails v3 apps to any platform +# +# Uses Zig as C compiler + macOS SDK for darwin targets +# +# Usage: +# docker build -t wails-cross -f Dockerfile.cross . +# docker run --rm -v $(pwd):/app wails-cross darwin arm64 +# docker run --rm -v $(pwd):/app wails-cross darwin amd64 +# docker run --rm -v $(pwd):/app wails-cross linux amd64 +# docker run --rm -v $(pwd):/app wails-cross linux arm64 +# docker run --rm -v $(pwd):/app wails-cross windows amd64 +# docker run --rm -v $(pwd):/app wails-cross windows arm64 + +FROM golang:1.25-alpine + +RUN apk add --no-cache curl xz nodejs npm + +# Install Zig +ARG ZIG_VERSION=0.14.0 +RUN curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \ + | tar -xJ -C /opt \ + && ln -s /opt/zig-linux-x86_64-${ZIG_VERSION}/zig /usr/local/bin/zig + +# Download macOS SDK (required for darwin targets) +ARG MACOS_SDK_VERSION=14.5 +RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \ + | tar -xJ -C /opt \ + && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk + +ENV MACOS_SDK_PATH=/opt/macos-sdk + +# Create zig cc wrappers for each target +# Darwin arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-arm64 + +# Darwin amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-amd64 + +# Linux amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target x86_64-linux-musl $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-linux-amd64 + +# Linux arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target aarch64-linux-musl $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-linux-arm64 + +# Windows amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target x86_64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-amd64 + +# Windows arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target aarch64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-arm64 + +# Build script +COPY <<'SCRIPT' /usr/local/bin/build.sh +#!/bin/sh +set -e + +OS=${1:-darwin} +ARCH=${2:-arm64} + +case "${OS}-${ARCH}" in + darwin-arm64|darwin-aarch64) export CC=zcc-darwin-arm64; export GOARCH=arm64; export GOOS=darwin ;; + darwin-amd64|darwin-x86_64) export CC=zcc-darwin-amd64; export GOARCH=amd64; export GOOS=darwin ;; + linux-arm64|linux-aarch64) export CC=zcc-linux-arm64; export GOARCH=arm64; export GOOS=linux ;; + linux-amd64|linux-x86_64) export CC=zcc-linux-amd64; export GOARCH=amd64; export GOOS=linux ;; + windows-arm64|windows-aarch64) export CC=zcc-windows-arm64; export GOARCH=arm64; export GOOS=windows ;; + windows-amd64|windows-x86_64) export CC=zcc-windows-amd64; export GOARCH=amd64; export GOOS=windows ;; + *) echo "Usage: "; echo " os: darwin, linux, windows"; echo " arch: amd64, arm64"; exit 1 ;; +esac + +export CGO_ENABLED=1 +export CGO_CFLAGS="-w" + +# Build frontend if exists and not already built (host may have built it) +if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then + (cd frontend && npm install --silent && npm run build --silent) +fi + +# Build +APP=${APP_NAME:-$(basename $(pwd))} +mkdir -p bin + +EXT="" +LDFLAGS="-s -w" +if [ "$GOOS" = "windows" ]; then + EXT=".exe" + LDFLAGS="-s -w -H windowsgui" +fi + +go build -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . +echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}" +SCRIPT +RUN chmod +x /usr/local/bin/build.sh + +WORKDIR /app +ENTRYPOINT ["/usr/local/bin/build.sh"] +CMD ["darwin", "arm64"] diff --git a/wails/build/ios/Assets.xcassets b/wails/build/ios/Assets.xcassets new file mode 100644 index 0000000..46fbb87 --- /dev/null +++ b/wails/build/ios/Assets.xcassets @@ -0,0 +1,116 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "images" : [ + { + "filename" : "icon-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "icon-20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "icon-20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "icon-1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ] +} \ No newline at end of file diff --git a/wails/build/ios/Info.dev.plist b/wails/build/ios/Info.dev.plist new file mode 100644 index 0000000..3110cc5 --- /dev/null +++ b/wails/build/ios/Info.dev.plist @@ -0,0 +1,62 @@ + + + + + CFBundleExecutable + videoconcat + CFBundleIdentifier + com.example.videoconcat.dev + CFBundleName + My Product (Dev) + CFBundleDisplayName + My Product (Dev) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0-dev + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + WailsDevelopmentMode + + + NSHumanReadableCopyright + © 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/wails/build/ios/Info.plist b/wails/build/ios/Info.plist new file mode 100644 index 0000000..4c9035f --- /dev/null +++ b/wails/build/ios/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleExecutable + videoconcat + CFBundleIdentifier + com.example.videoconcat + CFBundleName + My Product + CFBundleDisplayName + My Product + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + NSHumanReadableCopyright + © 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/wails/build/ios/LaunchScreen.storyboard b/wails/build/ios/LaunchScreen.storyboard new file mode 100644 index 0000000..5fb5a0a --- /dev/null +++ b/wails/build/ios/LaunchScreen.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wails/build/ios/Taskfile.yml b/wails/build/ios/Taskfile.yml new file mode 100644 index 0000000..8c27f08 --- /dev/null +++ b/wails/build/ios/Taskfile.yml @@ -0,0 +1,293 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + BUNDLE_ID: '{{.BUNDLE_ID | default "com.wails.app"}}' + # SDK_PATH is computed lazily at task-level to avoid errors on non-macOS systems + # Each task that needs it defines SDK_PATH in its own vars section + +tasks: + install:deps: + summary: Check and install iOS development dependencies + cmds: + - go run build/ios/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}' + prompt: This will check and install iOS development dependencies. Continue? + + # Note: Bindings generation may show CGO warnings for iOS C imports. + # These warnings are harmless and don't affect the generated bindings, + # as the generator only needs to parse Go types, not C implementations. + build: + summary: Creates a build of the application for iOS + deps: + - task: generate:ios:overlay + - task: generate:ios:xcode + - task: common:go:mod:tidy + - task: generate:ios:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building iOS app {{.APP_NAME}}..." + - go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json {{.BUILD_FLAGS}} -o {{.OUTPUT}}.a + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,ios -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags ios,debug -buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0' + CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator' + + compile:objc: + summary: Compile Objective-C iOS wrapper + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + create:app:bundle: + summary: Creates an iOS `.app` bundle + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - | + # Compile asset catalog and embed icons in the app bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + deploy-simulator: + summary: Deploy to iOS Simulator + deps: [package] + cmds: + - xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - xcrun simctl launch booted {{.BUNDLE_ID}} + + compile:ios: + summary: Compile the iOS executable from Go archive and main.m + deps: + - task: build + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - | + MAIN_M=build/ios/xcode/main/main.m + if [ ! -f "$MAIN_M" ]; then + MAIN_M=build/ios/main.m + fi + xcrun -sdk iphonesimulator clang \ + -target arm64-apple-ios15.0-simulator \ + -isysroot {{.SDK_PATH}} \ + -framework Foundation -framework UIKit -framework WebKit \ + -framework Security -framework CoreFoundation \ + -lresolv \ + -o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \ + "$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a" + + generate:ios:bindings: + internal: true + summary: Generates bindings for iOS with proper CGO flags + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0' + CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator' + + ensure-simulator: + internal: true + summary: Ensure iOS Simulator is running and booted + silent: true + cmds: + - | + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Starting iOS Simulator..." + # Get first available iPhone device + DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -o "[A-F0-9-]\{36\}" || true) + if [ -z "$DEVICE_ID" ]; then + echo "No iPhone simulator found. Creating one..." + RUNTIME=$(xcrun simctl list runtimes | grep iOS | tail -1 | awk '{print $NF}') + DEVICE_ID=$(xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "$RUNTIME") + fi + # Boot the device + echo "Booting device $DEVICE_ID..." + xcrun simctl boot "$DEVICE_ID" 2>/dev/null || true + # Open Simulator app + open -a Simulator + # Wait for boot (max 30 seconds) + for i in {1..30}; do + if xcrun simctl list devices booted | grep -q "Booted"; then + echo "Simulator booted successfully" + break + fi + sleep 1 + done + # Final check + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Failed to boot simulator after 30 seconds" + exit 1 + fi + fi + preconditions: + - sh: command -v xcrun + msg: "xcrun not found. Please run 'wails3 task ios:install:deps' to install iOS development dependencies" + + generate:ios:overlay: + internal: true + summary: Generate Go build overlay and iOS shim + sources: + - build/config.yml + generates: + - build/ios/xcode/overlay.json + - build/ios/xcode/gen/main_ios.gen.go + cmds: + - wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml + + generate:ios:xcode: + internal: true + summary: Generate iOS Xcode project structure and assets + sources: + - build/config.yml + - build/appicon.png + generates: + - build/ios/xcode/main/main.m + - build/ios/xcode/main/Assets.xcassets/**/* + - build/ios/xcode/project.pbxproj + cmds: + - wails3 ios xcode:gen -outdir build/ios/xcode -config build/config.yml + + run: + summary: Run the application in iOS Simulator + deps: + - task: ensure-simulator + - task: compile:ios + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}" + - cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist" + - | + # Compile asset catalog and embed icons for dev bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev" + + xcode: + summary: Open the generated Xcode project for this app + cmds: + - task: generate:ios:xcode + - open build/ios/xcode/main.xcodeproj + + logs: + summary: Stream iOS Simulator logs filtered to this app + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] "{{.APP_NAME | lower}}.app/" OR composedMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR eventMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR process == "{{.APP_NAME | lower}}" OR category CONTAINS[c] "{{.APP_NAME | lower}}"' + + logs:dev: + summary: Stream logs for the dev bundle (used by `task ios:run`) + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".dev.app/" OR subsystem == "com.wails.{{.APP_NAME | lower}}.dev" OR process == "{{.APP_NAME | lower}}"' + + logs:wide: + summary: Wide log stream to help discover the exact process/bundle identifiers + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".app/"' \ No newline at end of file diff --git a/wails/build/ios/app_options_default.go b/wails/build/ios/app_options_default.go new file mode 100644 index 0000000..04e4f1b --- /dev/null +++ b/wails/build/ios/app_options_default.go @@ -0,0 +1,10 @@ +//go:build !ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS is a no-op on non-iOS platforms +func modifyOptionsForIOS(opts *application.Options) { + // No modifications needed for non-iOS platforms +} \ No newline at end of file diff --git a/wails/build/ios/app_options_ios.go b/wails/build/ios/app_options_ios.go new file mode 100644 index 0000000..8f6ac31 --- /dev/null +++ b/wails/build/ios/app_options_ios.go @@ -0,0 +1,11 @@ +//go:build ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS adjusts the application options for iOS +func modifyOptionsForIOS(opts *application.Options) { + // Disable signal handlers on iOS to prevent crashes + opts.DisableDefaultSignalHandler = true +} \ No newline at end of file diff --git a/wails/build/ios/build.sh b/wails/build/ios/build.sh new file mode 100644 index 0000000..c0b88de --- /dev/null +++ b/wails/build/ios/build.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +# Build configuration +APP_NAME="videoconcat" +BUNDLE_ID="com.example.videoconcat" +VERSION="0.1.0" +BUILD_NUMBER="0.1.0" +BUILD_DIR="build/ios" +TARGET="simulator" + +echo "Building iOS app: $APP_NAME" +echo "Bundle ID: $BUNDLE_ID" +echo "Version: $VERSION ($BUILD_NUMBER)" +echo "Target: $TARGET" + +# Ensure build directory exists +mkdir -p "$BUILD_DIR" + +# Determine SDK and target architecture +if [ "$TARGET" = "simulator" ]; then + SDK="iphonesimulator" + ARCH="arm64-apple-ios15.0-simulator" +elif [ "$TARGET" = "device" ]; then + SDK="iphoneos" + ARCH="arm64-apple-ios15.0" +else + echo "Unknown target: $TARGET" + exit 1 +fi + +# Get SDK path +SDK_PATH=$(xcrun --sdk $SDK --show-sdk-path) + +# Compile the application +echo "Compiling with SDK: $SDK" +xcrun -sdk $SDK clang \ + -target $ARCH \ + -isysroot "$SDK_PATH" \ + -framework Foundation \ + -framework UIKit \ + -framework WebKit \ + -framework CoreGraphics \ + -o "$BUILD_DIR/$APP_NAME" \ + "$BUILD_DIR/main.m" + +# Create app bundle +echo "Creating app bundle..." +APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" +rm -rf "$APP_BUNDLE" +mkdir -p "$APP_BUNDLE" + +# Move executable +mv "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/" + +# Copy Info.plist +cp "$BUILD_DIR/Info.plist" "$APP_BUNDLE/" + +# Sign the app +echo "Signing app..." +codesign --force --sign - "$APP_BUNDLE" + +echo "Build complete: $APP_BUNDLE" + +# Deploy to simulator if requested +if [ "$TARGET" = "simulator" ]; then + echo "Deploying to simulator..." + xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true + xcrun simctl install booted "$APP_BUNDLE" + xcrun simctl launch booted "$BUNDLE_ID" + echo "App launched on simulator" +fi \ No newline at end of file diff --git a/wails/build/ios/entitlements.plist b/wails/build/ios/entitlements.plist new file mode 100644 index 0000000..cc5d958 --- /dev/null +++ b/wails/build/ios/entitlements.plist @@ -0,0 +1,21 @@ + + + + + + get-task-allow + + + + com.apple.security.app-sandbox + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-only + + + \ No newline at end of file diff --git a/wails/build/ios/icon.png b/wails/build/ios/icon.png new file mode 100644 index 0000000..be7d591 --- /dev/null +++ b/wails/build/ios/icon.png @@ -0,0 +1,3 @@ +# iOS Icon Placeholder +# This file should be replaced with the actual app icon (1024x1024 PNG) +# The build process will generate all required icon sizes from this base icon \ No newline at end of file diff --git a/wails/build/ios/main.m b/wails/build/ios/main.m new file mode 100644 index 0000000..366767a --- /dev/null +++ b/wails/build/ios/main.m @@ -0,0 +1,23 @@ +//go:build ios +// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate) +#import +#include + +// External Go initialization function from the c-archive (declare before use) +extern void WailsIOSMain(); + +int main(int argc, char * argv[]) { + @autoreleasepool { + // Disable buffering so stdout/stderr from Go log.Printf flush immediately + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + // Start Go runtime on a background queue to avoid blocking main thread/UI + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + WailsIOSMain(); + }); + + // Run UIApplicationMain using WailsAppDelegate provided by the Go archive + return UIApplicationMain(argc, argv, nil, @"WailsAppDelegate"); + } +} \ No newline at end of file diff --git a/wails/build/ios/main_ios.go b/wails/build/ios/main_ios.go new file mode 100644 index 0000000..b75a403 --- /dev/null +++ b/wails/build/ios/main_ios.go @@ -0,0 +1,24 @@ +//go:build ios + +package main + +import ( + "C" +) + +// For iOS builds, we need to export a function that can be called from Objective-C +// This wrapper allows us to keep the original main.go unmodified + +//export WailsIOSMain +func WailsIOSMain() { + // DO NOT lock the goroutine to the current OS thread on iOS! + // This causes signal handling issues: + // "signal 16 received on thread with no signal stack" + // "fatal error: non-Go code disabled sigaltstack" + // iOS apps run in a sandboxed environment where the Go runtime's + // signal handling doesn't work the same way as desktop platforms. + + // Call the actual main function from main.go + // This ensures all the user's code is executed + main() +} \ No newline at end of file diff --git a/wails/build/ios/project.pbxproj b/wails/build/ios/project.pbxproj new file mode 100644 index 0000000..6baf3f9 --- /dev/null +++ b/wails/build/ios/project.pbxproj @@ -0,0 +1,222 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C0DEBEEF0000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000002 /* main.m */; }; + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000101 /* UIKit.framework */; }; + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000102 /* Foundation.framework */; }; + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000103 /* WebKit.framework */; }; + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000104 /* Security.framework */; }; + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000105 /* CoreFoundation.framework */; }; + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000106 /* libresolv.tbd */; }; + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000107 /* My Product.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + C0DEBEEF0000000000000002 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0DEBEEF0000000000000004 /* My Product.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Product.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0DEBEEF0000000000000101 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000102 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000103 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000104 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000105 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000106 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.text-based-dylib-definition; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000107 /* My Product.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "My Product.a"; path = ../../../bin/My Product.a; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + C0DEBEEF0000000000000010 = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000020 /* Products */, + C0DEBEEF0000000000000045 /* Frameworks */, + C0DEBEEF0000000000000030 /* main */, + ); + sourceTree = ""; + }; + C0DEBEEF0000000000000020 /* Products */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000004 /* My Product.app */, + ); + name = Products; + sourceTree = ""; + }; + C0DEBEEF0000000000000030 /* main */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000002 /* main.m */, + C0DEBEEF0000000000000003 /* Info.plist */, + ); + path = main; + sourceTree = SOURCE_ROOT; + }; + C0DEBEEF0000000000000045 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000101 /* UIKit.framework */, + C0DEBEEF0000000000000102 /* Foundation.framework */, + C0DEBEEF0000000000000103 /* WebKit.framework */, + C0DEBEEF0000000000000104 /* Security.framework */, + C0DEBEEF0000000000000105 /* CoreFoundation.framework */, + C0DEBEEF0000000000000106 /* libresolv.tbd */, + C0DEBEEF0000000000000107 /* My Product.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C0DEBEEF0000000000000040 /* My Product */ = { + isa = PBXNativeTarget; + buildConfigurationList = C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */; + buildPhases = ( + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */, + C0DEBEEF0000000000000050 /* Sources */, + C0DEBEEF0000000000000056 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "My Product"; + productName = "My Product"; + productReference = C0DEBEEF0000000000000004 /* My Product.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C0DEBEEF0000000000000060 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1500; + ORGANIZATIONNAME = "My Company"; + TargetAttributes = { + C0DEBEEF0000000000000040 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = C0DEBEEF0000000000000010; + productRefGroup = C0DEBEEF0000000000000020 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C0DEBEEF0000000000000040 /* My Product */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXFrameworksBuildPhase section */ + C0DEBEEF0000000000000056 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */, + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */, + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */, + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */, + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */, + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */, + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Prebuild: Wails Go Archive"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\nAPP_ROOT=\"${PROJECT_DIR}/../../..\"\nSDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path)\nexport GOOS=ios\nexport GOARCH=arm64\nexport CGO_ENABLED=1\nexport CGO_CFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0\"\nexport CGO_LDFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator\"\ncd \"${APP_ROOT}\"\n# Ensure overlay exists\nif [ ! -f build/ios/xcode/overlay.json ]; then\n wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml || true\nfi\n# Build Go c-archive if missing or older than sources\nif [ ! -f bin/My Product.a ]; then\n echo \"Building Go c-archive...\"\n go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json -o bin/My Product.a\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C0DEBEEF0000000000000050 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF0000000000000001 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C0DEBEEF0000000000000090 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.videoconcat"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Debug; + }; + C0DEBEEF00000000000000A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.videoconcat"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = C0DEBEEF0000000000000060 /* Project object */; +} diff --git a/wails/build/ios/scripts/deps/install_deps.go b/wails/build/ios/scripts/deps/install_deps.go new file mode 100644 index 0000000..88ed47a --- /dev/null +++ b/wails/build/ios/scripts/deps/install_deps.go @@ -0,0 +1,319 @@ +// install_deps.go - iOS development dependency checker +// This script checks for required iOS development tools. +// It's designed to be portable across different shells by using Go instead of shell scripts. +// +// Usage: +// go run install_deps.go # Interactive mode +// TASK_FORCE_YES=true go run install_deps.go # Auto-accept prompts +// CI=true go run install_deps.go # CI mode (auto-accept) + +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" +) + +type Dependency struct { + Name string + CheckFunc func() (bool, string) // Returns (success, details) + Required bool + InstallCmd []string + InstallMsg string + SuccessMsg string + FailureMsg string +} + +func main() { + fmt.Println("Checking iOS development dependencies...") + fmt.Println("=" + strings.Repeat("=", 50)) + fmt.Println() + + hasErrors := false + dependencies := []Dependency{ + { + Name: "Xcode", + CheckFunc: func() (bool, string) { + // Check if xcodebuild exists + if !checkCommand([]string{"xcodebuild", "-version"}) { + return false, "" + } + // Get version info + out, err := exec.Command("xcodebuild", "-version").Output() + if err != nil { + return false, "" + } + lines := strings.Split(string(out), "\n") + if len(lines) > 0 { + return true, strings.TrimSpace(lines[0]) + } + return true, "" + }, + Required: true, + InstallMsg: "Please install Xcode from the Mac App Store:\n https://apps.apple.com/app/xcode/id497799835\n Xcode is REQUIRED for iOS development (includes iOS SDKs, simulators, and frameworks)", + SuccessMsg: "✅ Xcode found", + FailureMsg: "❌ Xcode not found (REQUIRED)", + }, + { + Name: "Xcode Developer Path", + CheckFunc: func() (bool, string) { + // Check if xcode-select points to a valid Xcode path + out, err := exec.Command("xcode-select", "-p").Output() + if err != nil { + return false, "xcode-select not configured" + } + path := strings.TrimSpace(string(out)) + + // Check if path exists and is in Xcode.app + if _, err := os.Stat(path); err != nil { + return false, "Invalid Xcode path" + } + + // Verify it's pointing to Xcode.app (not just Command Line Tools) + if !strings.Contains(path, "Xcode.app") { + return false, fmt.Sprintf("Points to %s (should be Xcode.app)", path) + } + + return true, path + }, + Required: true, + InstallCmd: []string{"sudo", "xcode-select", "-s", "/Applications/Xcode.app/Contents/Developer"}, + InstallMsg: "Xcode developer path needs to be configured", + SuccessMsg: "✅ Xcode developer path configured", + FailureMsg: "❌ Xcode developer path not configured correctly", + }, + { + Name: "iOS SDK", + CheckFunc: func() (bool, string) { + // Get the iOS Simulator SDK path + cmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path") + output, err := cmd.Output() + if err != nil { + return false, "Cannot find iOS SDK" + } + sdkPath := strings.TrimSpace(string(output)) + + // Check if the SDK path exists + if _, err := os.Stat(sdkPath); err != nil { + return false, "iOS SDK path not found" + } + + // Check for UIKit framework (essential for iOS development) + uikitPath := fmt.Sprintf("%s/System/Library/Frameworks/UIKit.framework", sdkPath) + if _, err := os.Stat(uikitPath); err != nil { + return false, "UIKit.framework not found" + } + + // Get SDK version + versionCmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version") + versionOut, _ := versionCmd.Output() + version := strings.TrimSpace(string(versionOut)) + + return true, fmt.Sprintf("iOS %s SDK", version) + }, + Required: true, + InstallMsg: "iOS SDK comes with Xcode. Please ensure Xcode is properly installed.", + SuccessMsg: "✅ iOS SDK found with UIKit framework", + FailureMsg: "❌ iOS SDK not found or incomplete", + }, + { + Name: "iOS Simulator Runtime", + CheckFunc: func() (bool, string) { + if !checkCommand([]string{"xcrun", "simctl", "help"}) { + return false, "" + } + // Check if we can list runtimes + out, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + return false, "Cannot access simulator" + } + // Count iOS runtimes + lines := strings.Split(string(out), "\n") + count := 0 + var versions []string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + count++ + // Extract version number + if parts := strings.Fields(line); len(parts) > 2 { + for _, part := range parts { + if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") { + versions = append(versions, strings.Trim(part, "()")) + break + } + } + } + } + } + if count > 0 { + return true, fmt.Sprintf("%d runtime(s): %s", count, strings.Join(versions, ", ")) + } + return false, "No iOS runtimes installed" + }, + Required: true, + InstallMsg: "iOS Simulator runtimes come with Xcode. You may need to download them:\n Xcode → Settings → Platforms → iOS", + SuccessMsg: "✅ iOS Simulator runtime available", + FailureMsg: "❌ iOS Simulator runtime not available", + }, + } + + // Check each dependency + for _, dep := range dependencies { + success, details := dep.CheckFunc() + if success { + msg := dep.SuccessMsg + if details != "" { + msg = fmt.Sprintf("%s (%s)", dep.SuccessMsg, details) + } + fmt.Println(msg) + } else { + fmt.Println(dep.FailureMsg) + if details != "" { + fmt.Printf(" Details: %s\n", details) + } + if dep.Required { + hasErrors = true + if len(dep.InstallCmd) > 0 { + fmt.Println() + fmt.Println(" " + dep.InstallMsg) + fmt.Printf(" Fix command: %s\n", strings.Join(dep.InstallCmd, " ")) + if promptUser("Do you want to run this command?") { + fmt.Println("Running command...") + cmd := exec.Command(dep.InstallCmd[0], dep.InstallCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + fmt.Printf("Command failed: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Command completed. Please run this check again.") + } else { + fmt.Printf(" Please run manually: %s\n", strings.Join(dep.InstallCmd, " ")) + } + } else { + fmt.Println(" " + dep.InstallMsg) + } + } + } + } + + // Check for iPhone simulators + fmt.Println() + fmt.Println("Checking for iPhone simulator devices...") + if !checkCommand([]string{"xcrun", "simctl", "list", "devices"}) { + fmt.Println("❌ Cannot check for iPhone simulators") + hasErrors = true + } else { + out, err := exec.Command("xcrun", "simctl", "list", "devices").Output() + if err != nil { + fmt.Println("❌ Failed to list simulator devices") + hasErrors = true + } else if !strings.Contains(string(out), "iPhone") { + fmt.Println("⚠️ No iPhone simulator devices found") + fmt.Println() + + // Get the latest iOS runtime + runtimeOut, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + fmt.Println(" Failed to get iOS runtimes:", err) + } else { + lines := strings.Split(string(runtimeOut), "\n") + var latestRuntime string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + // Extract runtime identifier + parts := strings.Fields(line) + if len(parts) > 0 { + latestRuntime = parts[len(parts)-1] + } + } + } + + if latestRuntime == "" { + fmt.Println(" No iOS runtime found. Please install iOS simulators in Xcode:") + fmt.Println(" Xcode → Settings → Platforms → iOS") + } else { + fmt.Println(" Would you like to create an iPhone 15 Pro simulator?") + createCmd := []string{"xcrun", "simctl", "create", "iPhone 15 Pro", "iPhone 15 Pro", latestRuntime} + fmt.Printf(" Command: %s\n", strings.Join(createCmd, " ")) + if promptUser("Create simulator?") { + cmd := exec.Command(createCmd[0], createCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf(" Failed to create simulator: %v\n", err) + } else { + fmt.Println(" ✅ iPhone 15 Pro simulator created") + } + } else { + fmt.Println(" Skipping simulator creation") + fmt.Printf(" Create manually: %s\n", strings.Join(createCmd, " ")) + } + } + } + } else { + // Count iPhone devices + count := 0 + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if strings.Contains(line, "iPhone") && !strings.Contains(line, "unavailable") { + count++ + } + } + fmt.Printf("✅ %d iPhone simulator device(s) available\n", count) + } + } + + // Final summary + fmt.Println() + fmt.Println("=" + strings.Repeat("=", 50)) + if hasErrors { + fmt.Println("❌ Some required dependencies are missing or misconfigured.") + fmt.Println() + fmt.Println("Quick setup guide:") + fmt.Println("1. Install Xcode from Mac App Store (if not installed)") + fmt.Println("2. Open Xcode once and agree to the license") + fmt.Println("3. Install additional components when prompted") + fmt.Println("4. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer") + fmt.Println("5. Download iOS simulators: Xcode → Settings → Platforms → iOS") + fmt.Println("6. Run this check again") + os.Exit(1) + } else { + fmt.Println("✅ All required dependencies are installed!") + fmt.Println(" You're ready for iOS development with Wails!") + } +} + +func checkCommand(args []string) bool { + if len(args) == 0 { + return false + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = nil + cmd.Stderr = nil + err := cmd.Run() + return err == nil +} + +func promptUser(question string) bool { + // Check if we're in a non-interactive environment + if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" { + fmt.Printf("%s [y/N]: y (auto-accepted)\n", question) + return true + } + + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", question) + + response, err := reader.ReadString('\n') + if err != nil { + return false + } + + response = strings.ToLower(strings.TrimSpace(response)) + return response == "y" || response == "yes" +} \ No newline at end of file diff --git a/wails/build/linux/Taskfile.yml b/wails/build/linux/Taskfile.yml new file mode 100644 index 0000000..c295586 --- /dev/null +++ b/wails/build/linux/Taskfile.yml @@ -0,0 +1,220 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # PGP_KEY: "path/to/signing-key.asc" + # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation (used when building on non-Linux or no CC available) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Linux + cmds: + # Linux requires CGO - use Docker when cross-compiling from non-Linux OR when no C compiler is available + - task: '{{if and (eq OS "linux") (eq .HAS_CC "true")}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Check if a C compiler is available (gcc or clang) + HAS_CC: + sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"' + + build:native: + summary: Builds the application natively on Linux + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + - task: generate:dotdesktop + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Cross-compiles for Linux using Docker with Zig (for macOS/Windows hosts) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + - task: generate:dotdesktop + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation to Linux. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application for Linux + deps: + - task: build + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + - task: generate:dotdesktop + cmds: + - cp "{{.APP_BINARY}}" "{{.APP_NAME}}" + - cp ../../appicon.png "{{.APP_NAME}}.png" + - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '{{.APP_NAME}}.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: '{{.APP_NAME}}' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' + + sign:deb: + summary: Signs the DEB package + desc: | + Signs the .deb package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:deb + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:rpm: + summary: Signs the RPM package + desc: | + Signs the .rpm package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:rpm + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:packages: + summary: Signs all Linux packages (DEB and RPM) + desc: | + Signs both .deb and .rpm packages with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + cmds: + - task: sign:deb + - task: sign:rpm + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" diff --git a/wails/build/linux/appimage/build.sh b/wails/build/linux/appimage/build.sh new file mode 100644 index 0000000..85901c3 --- /dev/null +++ b/wails/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/wails/build/linux/desktop b/wails/build/linux/desktop new file mode 100644 index 0000000..d882808 --- /dev/null +++ b/wails/build/linux/desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=My Product +Comment=A VideoConcat application +# The Exec line includes %u to pass the URL to the application +Exec=/usr/local/bin/videoconcat %u +Terminal=false +Type=Application +Icon=videoconcat +Categories=Utility; +StartupWMClass=videoconcat + + diff --git a/wails/build/linux/nfpm/nfpm.yaml b/wails/build/linux/nfpm/nfpm.yaml new file mode 100644 index 0000000..6a04fc8 --- /dev/null +++ b/wails/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,67 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "videoconcat" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "A VideoConcat application" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/videoconcat" + dst: "/usr/local/bin/videoconcat" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/videoconcat.png" + - src: "./build/linux/videoconcat.desktop" + dst: "/usr/share/applications/videoconcat.desktop" + +# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 +depends: + - libgtk-3-0 + - libwebkit2gtk-4.1-0 + +# Distribution-specific overrides for different package formats and WebKit versions +overrides: + # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0) + rpm: + depends: + - gtk3 + - webkit2gtk4.1 + + # Arch Linux packages (WebKit 4.1) + archlinux: + depends: + - gtk3 + - webkit2gtk-4.1 + +# scripts section to ensure desktop database is updated after install +scripts: + postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + # You can also add preremove, postremove if needed + # preremove: "./build/linux/nfpm/scripts/preremove.sh" + # postremove: "./build/linux/nfpm/scripts/postremove.sh" + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" diff --git a/wails/build/linux/nfpm/scripts/postinstall.sh b/wails/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 0000000..4bbb815 --- /dev/null +++ b/wails/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Update desktop database for .desktop file changes +# This makes the application appear in application menus and registers its capabilities. +if command -v update-desktop-database >/dev/null 2>&1; then + echo "Updating desktop database..." + update-desktop-database -q /usr/share/applications +else + echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2 +fi + +# Update MIME database for custom URL schemes (x-scheme-handler) +# This ensures the system knows how to handle your custom protocols. +if command -v update-mime-database >/dev/null 2>&1; then + echo "Updating MIME database..." + update-mime-database -n /usr/share/mime +else + echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 +fi + +exit 0 diff --git a/wails/build/linux/nfpm/scripts/postremove.sh b/wails/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/wails/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/wails/build/linux/nfpm/scripts/preinstall.sh b/wails/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/wails/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/wails/build/linux/nfpm/scripts/preremove.sh b/wails/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/wails/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/wails/build/windows/Taskfile.yml b/wails/build/windows/Taskfile.yml new file mode 100644 index 0000000..77b620b --- /dev/null +++ b/wails/build/windows/Taskfile.yml @@ -0,0 +1,183 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_CERTIFICATE: "path/to/certificate.pfx" + # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE + # TIMESTAMP_SERVER: "http://timestamp.digicert.com" + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Windows + cmds: + # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile + - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + vars: + # Default to CGO_ENABLED=0 if not explicitly set + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + + build:native: + summary: Builds the application using native Go cross-compilation + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe" + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for CGO cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - task: generate:syso + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - rm -f *.syso + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application + cmds: + - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - | + {{if eq OS "windows"}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi + {{else}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + {{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' + + sign: + summary: Signs the Windows executable + desc: | + Signs the .exe with an Authenticode certificate. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: build + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" + + sign:installer: + summary: Signs the NSIS installer + desc: | + Creates and signs the NSIS installer. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:nsis:installer + cmds: + - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" diff --git a/wails/build/windows/icon.ico b/wails/build/windows/icon.ico new file mode 100644 index 0000000..bfa0690 Binary files /dev/null and b/wails/build/windows/icon.ico differ diff --git a/wails/build/windows/info.json b/wails/build/windows/info.json new file mode 100644 index 0000000..e7e9a2e --- /dev/null +++ b/wails/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "A VideoConcat application", + "LegalCopyright": "© 2026, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/wails/build/windows/msix/app_manifest.xml b/wails/build/windows/msix/app_manifest.xml new file mode 100644 index 0000000..260f12d --- /dev/null +++ b/wails/build/windows/msix/app_manifest.xml @@ -0,0 +1,55 @@ + + + + + + + My Product + My Company + A VideoConcat application + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wails/build/windows/msix/template.xml b/wails/build/windows/msix/template.xml new file mode 100644 index 0000000..3c42699 --- /dev/null +++ b/wails/build/windows/msix/template.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + false + My Product + My Company + A VideoConcat application + Assets\AppIcon.png + + + + + + + diff --git a/wails/build/windows/nsis/project.nsi b/wails/build/windows/nsis/project.nsi new file mode 100644 index 0000000..7196811 --- /dev/null +++ b/wails/build/windows/nsis/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "VideoConcat" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/wails/build/windows/nsis/wails_tools.nsh b/wails/build/windows/nsis/wails_tools.nsh new file mode 100644 index 0000000..0678466 --- /dev/null +++ b/wails/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,236 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "VideoConcat" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "© 2026, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + +!macroend \ No newline at end of file diff --git a/wails/build/windows/wails.exe.manifest b/wails/build/windows/wails.exe.manifest new file mode 100644 index 0000000..36b30fa --- /dev/null +++ b/wails/build/windows/wails.exe.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + + + + + + + \ No newline at end of file diff --git a/wails/config.json b/wails/config.json new file mode 100644 index 0000000..80fc603 --- /dev/null +++ b/wails/config.json @@ -0,0 +1,8 @@ +{ + "super_users": [ + { + "username": "super", + "password_hash": "将此处替换为密码的MD5 hash值(32位十六进制字符串)。密码'080500'的MD5 hash值会在程序启动时输出到日志中" + } + ] +} diff --git a/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js b/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js new file mode 100644 index 0000000..1ea1058 --- /dev/null +++ b/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..3dd1807 --- /dev/null +++ b/wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/wails/frontend/bindings/videoconcat/services/authservice.js b/wails/frontend/bindings/videoconcat/services/authservice.js new file mode 100644 index 0000000..91797d0 --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/authservice.js @@ -0,0 +1,32 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * AuthService 认证服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * Login 用户登录 + * @param {string} username + * @param {string} password + * @returns {$CancellablePromise<$models.LoginResponse | null>} + */ +export function Login(username, password) { + return $Call.ByID(2350837569, username, password).then(/** @type {($result: any) => any} */(($result) => { + return $$createType1($result); + })); +} + +// Private type creation functions +const $$createType0 = $models.LoginResponse.createFrom; +const $$createType1 = $Create.Nullable($$createType0); diff --git a/wails/frontend/bindings/videoconcat/services/extractservice.js b/wails/frontend/bindings/videoconcat/services/extractservice.js new file mode 100644 index 0000000..b8629f6 --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/extractservice.js @@ -0,0 +1,74 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * ExtractService 抽帧服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * ExtractFrames 批量抽帧 + * @param {$models.ExtractFrameRequest} req + * @returns {$CancellablePromise<$models.ExtractFrameResult[]>} + */ +export function ExtractFrames(req) { + return $Call.ByID(1728131056, req).then(/** @type {($result: any) => any} */(($result) => { + return $$createType1($result); + })); +} + +/** + * ListVideos 列出文件夹中的视频文件 + * @param {string} folderPath + * @returns {$CancellablePromise} + */ +export function ListVideos(folderPath) { + return $Call.ByID(2398906893, folderPath).then(/** @type {($result: any) => any} */(($result) => { + return $$createType2($result); + })); +} + +/** + * ModifyByMetadata 通过修改元数据改变文件 MD5 + * @param {string} inputPath + * @param {string} outputPath + * @returns {$CancellablePromise} + */ +export function ModifyByMetadata(inputPath, outputPath) { + return $Call.ByID(4068325271, inputPath, outputPath); +} + +/** + * ModifyVideosMetadata 批量修改视频元数据 + * @param {string} folderPath + * @returns {$CancellablePromise<$models.ExtractFrameResult[]>} + */ +export function ModifyVideosMetadata(folderPath) { + return $Call.ByID(3157640998, folderPath).then(/** @type {($result: any) => any} */(($result) => { + return $$createType1($result); + })); +} + +/** + * RemoveFrameRandom 随机删除视频中的一帧 + * @param {string} inputPath + * @param {string} outputPath + * @returns {$CancellablePromise} + */ +export function RemoveFrameRandom(inputPath, outputPath) { + return $Call.ByID(4062392693, inputPath, outputPath); +} + +// Private type creation functions +const $$createType0 = $models.ExtractFrameResult.createFrom; +const $$createType1 = $Create.Array($$createType0); +const $$createType2 = $Create.Array($Create.Any); diff --git a/wails/frontend/bindings/videoconcat/services/fileservice.js b/wails/frontend/bindings/videoconcat/services/fileservice.js new file mode 100644 index 0000000..56782cb --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/fileservice.js @@ -0,0 +1,80 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * FileService 文件服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +/** + * EnsureDirectory 确保目录存在 + * @param {string} dirPath + * @returns {$CancellablePromise} + */ +export function EnsureDirectory(dirPath) { + return $Call.ByID(1924614253, dirPath); +} + +/** + * FileExists 检查文件是否存在 + * @param {string} filePath + * @returns {$CancellablePromise} + */ +export function FileExists(filePath) { + return $Call.ByID(26080110, filePath); +} + +/** + * GetFileSize 获取文件大小(字节) + * @param {string} filePath + * @returns {$CancellablePromise} + */ +export function GetFileSize(filePath) { + return $Call.ByID(3113069571, filePath); +} + +/** + * ListFiles 列出目录中的文件 + * @param {string} dirPath + * @param {string} pattern + * @returns {$CancellablePromise} + */ +export function ListFiles(dirPath, pattern) { + return $Call.ByID(1922143815, dirPath, pattern).then(/** @type {($result: any) => any} */(($result) => { + return $$createType0($result); + })); +} + +/** + * OpenFolder 打开文件夹 + * @param {string} folderPath + * @returns {$CancellablePromise} + */ +export function OpenFolder(folderPath) { + return $Call.ByID(578638620, folderPath); +} + +/** + * SelectFile 选择文件(返回路径) + * @param {string} filter + * @returns {$CancellablePromise} + */ +export function SelectFile(filter) { + return $Call.ByID(2093145774, filter); +} + +/** + * SelectFolder 选择文件夹(返回路径) + * @returns {$CancellablePromise} + */ +export function SelectFolder() { + return $Call.ByID(23551676); +} + +// Private type creation functions +const $$createType0 = $Create.Array($Create.Any); diff --git a/wails/frontend/bindings/videoconcat/services/index.js b/wails/frontend/bindings/videoconcat/services/index.js new file mode 100644 index 0000000..fb39383 --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/index.js @@ -0,0 +1,23 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as AuthService from "./authservice.js"; +import * as ExtractService from "./extractservice.js"; +import * as FileService from "./fileservice.js"; +import * as VideoService from "./videoservice.js"; +export { + AuthService, + ExtractService, + FileService, + VideoService +}; + +export { + ExtractFrameRequest, + ExtractFrameResult, + FolderInfo, + LoginResponse, + VideoConcatRequest, + VideoConcatResult +} from "./models.js"; diff --git a/wails/frontend/bindings/videoconcat/services/models.js b/wails/frontend/bindings/videoconcat/services/models.js new file mode 100644 index 0000000..c47b124 --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/models.js @@ -0,0 +1,341 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * ExtractFrameRequest 抽帧请求 + */ +export class ExtractFrameRequest { + /** + * Creates a new ExtractFrameRequest instance. + * @param {Partial} [$$source = {}] - The source object to create the ExtractFrameRequest. + */ + constructor($$source = {}) { + if (!("folderPath" in $$source)) { + /** + * @member + * @type {string} + */ + this["folderPath"] = ""; + } + if (!("extractCount" in $$source)) { + /** + * 每个视频生成的数量 + * @member + * @type {number} + */ + this["extractCount"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ExtractFrameRequest instance from a string or object. + * @param {any} [$$source = {}] + * @returns {ExtractFrameRequest} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new ExtractFrameRequest(/** @type {Partial} */($$parsedSource)); + } +} + +/** + * ExtractFrameResult 抽帧结果 + */ +export class ExtractFrameResult { + /** + * Creates a new ExtractFrameResult instance. + * @param {Partial} [$$source = {}] - The source object to create the ExtractFrameResult. + */ + constructor($$source = {}) { + if (!("videoPath" in $$source)) { + /** + * @member + * @type {string} + */ + this["videoPath"] = ""; + } + if (!("outputPath" in $$source)) { + /** + * @member + * @type {string} + */ + this["outputPath"] = ""; + } + if (!("success" in $$source)) { + /** + * @member + * @type {boolean} + */ + this["success"] = false; + } + if (/** @type {any} */(false)) { + /** + * @member + * @type {string | undefined} + */ + this["error"] = undefined; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ExtractFrameResult instance from a string or object. + * @param {any} [$$source = {}] + * @returns {ExtractFrameResult} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new ExtractFrameResult(/** @type {Partial} */($$parsedSource)); + } +} + +/** + * FolderInfo 文件夹信息 + */ +export class FolderInfo { + /** + * Creates a new FolderInfo instance. + * @param {Partial} [$$source = {}] - The source object to create the FolderInfo. + */ + constructor($$source = {}) { + if (!("path" in $$source)) { + /** + * @member + * @type {string} + */ + this["path"] = ""; + } + if (!("name" in $$source)) { + /** + * @member + * @type {string} + */ + this["name"] = ""; + } + if (!("videoCount" in $$source)) { + /** + * @member + * @type {number} + */ + this["videoCount"] = 0; + } + if (!("videoPaths" in $$source)) { + /** + * @member + * @type {string[]} + */ + this["videoPaths"] = []; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new FolderInfo instance from a string or object. + * @param {any} [$$source = {}] + * @returns {FolderInfo} + */ + static createFrom($$source = {}) { + const $$createField3_0 = $$createType0; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("videoPaths" in $$parsedSource) { + $$parsedSource["videoPaths"] = $$createField3_0($$parsedSource["videoPaths"]); + } + return new FolderInfo(/** @type {Partial} */($$parsedSource)); + } +} + +/** + * LoginResponse 登录响应 + */ +export class LoginResponse { + /** + * Creates a new LoginResponse instance. + * @param {Partial} [$$source = {}] - The source object to create the LoginResponse. + */ + constructor($$source = {}) { + if (!("Code" in $$source)) { + /** + * @member + * @type {number} + */ + this["Code"] = 0; + } + if (!("Msg" in $$source)) { + /** + * @member + * @type {string} + */ + this["Msg"] = ""; + } + if (!("Data" in $$source)) { + /** + * @member + * @type {any} + */ + this["Data"] = null; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new LoginResponse instance from a string or object. + * @param {any} [$$source = {}] + * @returns {LoginResponse} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new LoginResponse(/** @type {Partial} */($$parsedSource)); + } +} + +/** + * VideoConcatRequest 视频拼接请求 + */ +export class VideoConcatRequest { + /** + * Creates a new VideoConcatRequest instance. + * @param {Partial} [$$source = {}] - The source object to create the VideoConcatRequest. + */ + constructor($$source = {}) { + if (!("folderPath" in $$source)) { + /** + * @member + * @type {string} + */ + this["folderPath"] = ""; + } + if (!("num" in $$source)) { + /** + * @member + * @type {number} + */ + this["num"] = 0; + } + if (!("joinType" in $$source)) { + /** + * 1: 组合拼接, 2: 顺序拼接 + * @member + * @type {number} + */ + this["joinType"] = 0; + } + if (!("auditImagePath" in $$source)) { + /** + * @member + * @type {string} + */ + this["auditImagePath"] = ""; + } + if (!("folderInfos" in $$source)) { + /** + * @member + * @type {FolderInfo[]} + */ + this["folderInfos"] = []; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new VideoConcatRequest instance from a string or object. + * @param {any} [$$source = {}] + * @returns {VideoConcatRequest} + */ + static createFrom($$source = {}) { + const $$createField4_0 = $$createType2; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("folderInfos" in $$parsedSource) { + $$parsedSource["folderInfos"] = $$createField4_0($$parsedSource["folderInfos"]); + } + return new VideoConcatRequest(/** @type {Partial} */($$parsedSource)); + } +} + +/** + * VideoConcatResult 视频拼接结果 + */ +export class VideoConcatResult { + /** + * Creates a new VideoConcatResult instance. + * @param {Partial} [$$source = {}] - The source object to create the VideoConcatResult. + */ + constructor($$source = {}) { + if (!("index" in $$source)) { + /** + * @member + * @type {number} + */ + this["index"] = 0; + } + if (!("fileName" in $$source)) { + /** + * @member + * @type {string} + */ + this["fileName"] = ""; + } + if (!("filePath" in $$source)) { + /** + * @member + * @type {string} + */ + this["filePath"] = ""; + } + if (!("size" in $$source)) { + /** + * @member + * @type {string} + */ + this["size"] = ""; + } + if (!("seconds" in $$source)) { + /** + * @member + * @type {number} + */ + this["seconds"] = 0; + } + if (!("status" in $$source)) { + /** + * @member + * @type {string} + */ + this["status"] = ""; + } + if (!("progress" in $$source)) { + /** + * @member + * @type {string} + */ + this["progress"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new VideoConcatResult instance from a string or object. + * @param {any} [$$source = {}] + * @returns {VideoConcatResult} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new VideoConcatResult(/** @type {Partial} */($$parsedSource)); + } +} + +// Private type creation functions +const $$createType0 = $Create.Array($Create.Any); +const $$createType1 = FolderInfo.createFrom; +const $$createType2 = $Create.Array($$createType1); diff --git a/wails/frontend/bindings/videoconcat/services/videoservice.js b/wails/frontend/bindings/videoconcat/services/videoservice.js new file mode 100644 index 0000000..16fee2c --- /dev/null +++ b/wails/frontend/bindings/videoconcat/services/videoservice.js @@ -0,0 +1,75 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * VideoService 视频处理服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * ConvertVideoToTS 将视频转换为 TS 格式 + * @param {string} videoPath + * @returns {$CancellablePromise} + */ +export function ConvertVideoToTS(videoPath) { + return $Call.ByID(2005037305, videoPath); +} + +/** + * GenerateCombinations 生成所有视频组合(笛卡尔积) + * @param {string[][]} videoLists + * @param {number} index + * @param {string[]} currentCombination + * @param {string[][] | null} result + * @returns {$CancellablePromise} + */ +export function GenerateCombinations(videoLists, index, currentCombination, result) { + return $Call.ByID(2802045808, videoLists, index, currentCombination, result); +} + +/** + * GetLargeFileMD5 计算大文件的 MD5 + * @param {string} filePath + * @returns {$CancellablePromise} + */ +export function GetLargeFileMD5(filePath) { + return $Call.ByID(229340298, filePath); +} + +/** + * JoinVideos 拼接视频 + * 注意:Wails3 中回调函数需要使用事件系统,这里先简化处理 + * @param {$models.VideoConcatRequest} req + * @returns {$CancellablePromise<$models.VideoConcatResult[]>} + */ +export function JoinVideos(req) { + return $Call.ByID(2660164351, req).then(/** @type {($result: any) => any} */(($result) => { + return $$createType1($result); + })); +} + +/** + * ListFolders 列出文件夹中的视频文件夹 + * @param {string} folderPath + * @returns {$CancellablePromise<$models.FolderInfo[]>} + */ +export function ListFolders(folderPath) { + return $Call.ByID(2209109868, folderPath).then(/** @type {($result: any) => any} */(($result) => { + return $$createType3($result); + })); +} + +// Private type creation functions +const $$createType0 = $models.VideoConcatResult.createFrom; +const $$createType1 = $Create.Array($$createType0); +const $$createType2 = $models.FolderInfo.createFrom; +const $$createType3 = $Create.Array($$createType2); diff --git a/wails3-app/frontend/index.html b/wails/frontend/index.html similarity index 100% rename from wails3-app/frontend/index.html rename to wails/frontend/index.html diff --git a/wails/frontend/package-lock.json b/wails/frontend/package-lock.json new file mode 100644 index 0000000..ee1d557 --- /dev/null +++ b/wails/frontend/package-lock.json @@ -0,0 +1,1216 @@ +{ + "name": "videoconcat-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "videoconcat-frontend", + "version": "1.0.0", + "dependencies": { + "@wailsio/runtime": "latest", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "license": "MIT" + }, + "node_modules/@wailsio/runtime": { + "version": "3.0.0-alpha.78", + "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.78.tgz", + "integrity": "sha512-OwupMaPV20+9rMJdPTSqlmmu3BaMUEZOdngFK2cAjW228ecO5mg5vq1VPx9op1dftbpKJCKqsVhOpxa0EEQnRA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/wails3-app/frontend/package.json b/wails/frontend/package.json similarity index 73% rename from wails3-app/frontend/package.json rename to wails/frontend/package.json index 99ce6e3..0a1bb5c 100644 --- a/wails3-app/frontend/package.json +++ b/wails/frontend/package.json @@ -5,10 +5,12 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:dev": "vite build --mode development", "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.0" + "vue": "^3.4.0", + "@wailsio/runtime": "latest" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", diff --git a/wails/frontend/src/App.vue b/wails/frontend/src/App.vue new file mode 100644 index 0000000..67a9f26 --- /dev/null +++ b/wails/frontend/src/App.vue @@ -0,0 +1,231 @@ + + + + + + diff --git a/wails3-app/frontend/src/components/ExtractTab.vue b/wails/frontend/src/components/ExtractTab.vue similarity index 84% rename from wails3-app/frontend/src/components/ExtractTab.vue rename to wails/frontend/src/components/ExtractTab.vue index 9ce181e..1e32bdd 100644 --- a/wails3-app/frontend/src/components/ExtractTab.vue +++ b/wails/frontend/src/components/ExtractTab.vue @@ -69,14 +69,7 @@ - - - diff --git a/wails3-app/go.mod b/wails3-app/go.mod deleted file mode 100644 index 419ce59..0000000 --- a/wails3-app/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module videoconcat - -go 1.21 - -require ( - github.com/wailsapp/wails/v3 v3.0.0-alpha.57 -) - -require ( - github.com/google/uuid v1.3.0 // indirect -) - diff --git a/wails3-app/services/auth_service.go b/wails3-app/services/auth_service.go deleted file mode 100644 index fc500b7..0000000 --- a/wails3-app/services/auth_service.go +++ /dev/null @@ -1,104 +0,0 @@ -package services - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "time" -) - -// AuthService 认证服务 -type AuthService struct { - baseURL string - client *http.Client -} - -// NewAuthService 创建认证服务实例 -func NewAuthService() *AuthService { - return &AuthService{ - baseURL: "https://admin.xiangbing.vip", - client: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// LoginRequest 登录请求 -type LoginRequest struct { - Username string `json:"Username"` - Password string `json:"Password"` - Platform string `json:"Platform"` - PcName string `json:"PcName"` - PcUserName string `json:"PcUserName"` - Ips string `json:"Ips"` -} - -// LoginResponse 登录响应 -type LoginResponse struct { - Code int `json:"Code"` - Msg string `json:"Msg"` - Data interface{} `json:"Data"` -} - -// Login 用户登录 -func (s *AuthService) Login(ctx context.Context, username, password string) (*LoginResponse, error) { - // 获取机器信息 - pcMachineName, _ := os.Hostname() - pcUserName := os.Getenv("USERNAME") - if pcUserName == "" { - pcUserName = os.Getenv("USER") - } - - // 获取 IP 地址 - var ips string - addrs, err := net.InterfaceAddrs() - if err == nil { - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - ips = ipnet.IP.String() - break - } - } - } - } - - reqData := LoginRequest{ - Username: username, - Password: password, - Platform: "pc", - PcName: pcMachineName, - PcUserName: pcUserName, - Ips: ips, - } - - jsonData, err := json.Marshal(reqData) - if err != nil { - return nil, fmt.Errorf("序列化请求数据失败: %v", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/api/base/login", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("创建请求失败: %v", err) - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := s.client.Do(req) - if err != nil { - return nil, fmt.Errorf("请求失败: %v", err) - } - defer resp.Body.Close() - - var loginResp LoginResponse - if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil { - return nil, fmt.Errorf("解析响应失败: %v", err) - } - - return &loginResp, nil -} - diff --git a/wails3-app/services/file_service.go b/wails3-app/services/file_service.go deleted file mode 100644 index e2ff66e..0000000 --- a/wails3-app/services/file_service.go +++ /dev/null @@ -1,76 +0,0 @@ -package services - -import ( - "context" - "os" - "path/filepath" -) - -// FileService 文件服务 -type FileService struct{} - -// NewFileService 创建文件服务实例 -func NewFileService() *FileService { - return &FileService{} -} - -// SelectFolder 选择文件夹(返回路径) -func (s *FileService) SelectFolder(ctx context.Context) (string, error) { - // 在 Wails3 中,文件选择需要通过前端实现 - // 这里只是占位,实际应该通过前端调用系统对话框 - return "", nil -} - -// SelectFile 选择文件(返回路径) -func (s *FileService) SelectFile(ctx context.Context, filter string) (string, error) { - // 在 Wails3 中,文件选择需要通过前端实现 - return "", nil -} - -// OpenFolder 打开文件夹 -func (s *FileService) OpenFolder(ctx context.Context, folderPath string) error { - // Windows 下打开文件夹 - cmd := "explorer" - args := []string{folderPath} - - // 这里需要使用 exec.Command,但为了简化,我们返回路径让前端处理 - // 或者使用系统调用 - return nil -} - -// FileExists 检查文件是否存在 -func (s *FileService) FileExists(ctx context.Context, filePath string) (bool, error) { - _, err := os.Stat(filePath) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} - -// GetFileSize 获取文件大小(字节) -func (s *FileService) GetFileSize(ctx context.Context, filePath string) (int64, error) { - info, err := os.Stat(filePath) - if err != nil { - return 0, err - } - return info.Size(), nil -} - -// EnsureDirectory 确保目录存在 -func (s *FileService) EnsureDirectory(ctx context.Context, dirPath string) error { - return os.MkdirAll(dirPath, 0755) -} - -// ListFiles 列出目录中的文件 -func (s *FileService) ListFiles(ctx context.Context, dirPath string, pattern string) ([]string, error) { - patternPath := filepath.Join(dirPath, pattern) - matches, err := filepath.Glob(patternPath) - if err != nil { - return nil, err - } - return matches, nil -} - diff --git a/wails3-app/services/log.go b/wails3-app/services/log.go deleted file mode 100644 index 15e463e..0000000 --- a/wails3-app/services/log.go +++ /dev/null @@ -1,60 +0,0 @@ -package services - -import ( - "fmt" - "log" - "os" - "path/filepath" - "time" -) - -var logDir string - -func init() { - // 设置日志目录 - exePath, err := os.Executable() - if err != nil { - logDir = "./Log" - } else { - logDir = filepath.Join(filepath.Dir(exePath), "Log") - } - os.MkdirAll(logDir, 0755) -} - -// LogInfo 记录信息日志 -func LogInfo(message string) { - logMessage("INFO", message) -} - -// LogError 记录错误日志 -func LogError(message string) { - logMessage("ERROR", message) -} - -// LogWarn 记录警告日志 -func LogWarn(message string) { - logMessage("WARN", message) -} - -// LogDebug 记录调试日志 -func LogDebug(message string) { - logMessage("DEBUG", message) -} - -// logMessage 写入日志消息 -func logMessage(level, message string) { - // 控制台输出 - log.Printf("[%s] %s", level, message) - - // 文件输出 - logFile := filepath.Join(logDir, fmt.Sprintf("log%s.log", time.Now().Format("20060102"))) - file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err != nil { - return - } - defer file.Close() - - timestamp := time.Now().Format("15:04:05") - file.WriteString(fmt.Sprintf("%s [%s] %s\n", timestamp, level, message)) -} - diff --git a/wails3-app/快速开始.md b/wails3-app/快速开始.md deleted file mode 100644 index 560cbbb..0000000 --- a/wails3-app/快速开始.md +++ /dev/null @@ -1,100 +0,0 @@ -# 快速开始指南 - -## 解决 wails.json 错误 - -如果遇到 "wails.json: The system cannot find the file specified" 错误,请按以下步骤操作: - -### 步骤 1: 安装前端依赖 - -```powershell -cd frontend -npm install -``` - -### 步骤 2: 构建前端 - -```powershell -npm run build -``` - -这会生成 `assets` 目录和所有前端资源。 - -### 步骤 3: 运行应用 - -```powershell -cd .. -go run app.go -``` - -## 如果仍然遇到问题 - -### 检查文件是否存在 - -确保以下文件存在: -- `wails.json` ✓ (已创建) -- `assets/` 目录 ✓ (已创建) -- `assets/index.html` ✓ (已创建) - -### 如果 assets 目录为空 - -需要先构建前端: - -```powershell -cd frontend -npm install -npm run build -cd .. -``` - -### 验证 Go 模块 - -```powershell -go mod tidy -go mod download -``` - -### 检查 Wails3 版本 - -确保使用的是正确的 Wails3 版本。如果使用的是 Wails v2,配置方式可能不同。 - -## 开发模式 - -### 前端开发(热重载) - -```powershell -cd frontend -npm run dev -``` - -### 后端开发 - -在另一个终端: - -```powershell -go run app.go -``` - -## 常见错误解决 - -### 1. "module not found" - -```powershell -go mod tidy -go mod download -``` - -### 2. "assets not found" - -确保已构建前端: -```powershell -cd frontend -npm run build -``` - -### 3. "ffmpeg not found" - -确保 FFmpeg 已安装并在 PATH 中: -```powershell -ffmpeg -version -``` - diff --git a/wails3-app/构建说明.md b/wails3-app/构建说明.md deleted file mode 100644 index cef978c..0000000 --- a/wails3-app/构建说明.md +++ /dev/null @@ -1,151 +0,0 @@ -# 构建和运行说明 - -## 前置要求 - -1. **Go 1.21+** - ```bash - go version - ``` - -2. **Node.js 16+** - ```bash - node --version - npm --version - ``` - -3. **FFmpeg** - ```bash - ffmpeg -version - ``` - -4. **Wails3 CLI** - ```bash - # 安装 Wails3(如果还没有) - git clone https://github.com/wailsapp/wails.git - cd wails - git checkout v3-alpha - cd v3/cmd/wails3 - go install - ``` - -## 构建步骤 - -### 1. 安装前端依赖 - -```bash -cd frontend -npm install -``` - -### 2. 构建前端 - -```bash -npm run build -``` - -这会将前端代码构建到 `../assets` 目录。 - -### 3. 运行应用 - -```bash -cd .. -go run app.go -``` - -### 4. 构建可执行文件(可选) - -```bash -go build -o videoconcat.exe app.go -``` - -## 开发模式 - -### 前端开发 - -```bash -cd frontend -npm run dev -``` - -### 后端开发 - -```bash -go run app.go -``` - -## 常见问题 - -### 1. FFmpeg 未找到 - -**错误**: `exec: "ffmpeg": executable file not found in %PATH%` - -**解决**: -- Windows: 下载 FFmpeg 并添加到系统 PATH -- 或者修改代码指定 FFmpeg 完整路径 - -### 2. 前端构建失败 - -**错误**: `npm ERR!` - -**解决**: -```bash -# 清除缓存 -npm cache clean --force -# 删除 node_modules 重新安装 -rm -rf node_modules -npm install -``` - -### 3. Go 模块下载失败 - -**错误**: `go: module ... not found` - -**解决**: -```bash -# 设置 Go 代理(中国用户) -go env -w GOPROXY=https://goproxy.cn,direct -# 或者使用官方代理 -go env -w GOPROXY=https://proxy.golang.org,direct -``` - -### 4. Wails3 绑定失败 - -**错误**: `window.go is undefined` - -**解决**: -- 检查 Wails3 版本和 API -- 确保服务正确绑定 -- 查看浏览器控制台错误信息 - -## 调试 - -### 查看日志 - -日志文件位于:`Log/logYYYYMMDD.log` - -### 浏览器调试 - -1. 打开开发者工具(F12) -2. 查看 Console 标签页的错误信息 -3. 查看 Network 标签页的请求 - -### Go 调试 - -```bash -# 使用 delve 调试器 -go install github.com/go-delve/delve/cmd/dlv@latest -dlv debug app.go -``` - -## 打包发布 - -### Windows - -```bash -go build -ldflags="-H windowsgui" -o videoconcat.exe app.go -``` - -### 其他平台 - -参考 Wails3 官方文档的打包说明。 - diff --git a/wails3-app/迁移说明.md b/wails3-app/迁移说明.md deleted file mode 100644 index 64f3c80..0000000 --- a/wails3-app/迁移说明.md +++ /dev/null @@ -1,166 +0,0 @@ -# Wails3 重构迁移说明 - -## 已完成的工作 - -### 1. 项目结构 -- ✅ 创建了完整的 Wails3 项目结构 -- ✅ Go 后端服务(services/ 目录) -- ✅ Vue3 前端(frontend/ 目录) - -### 2. 后端功能实现 -- ✅ **VideoService**: 视频拼接服务 - - 列出文件夹中的视频 - - 视频格式转换(MP4 -> TS) - - 组合拼接和顺序拼接 - - MD5 计算和缓存机制 - -- ✅ **ExtractService**: 视频抽帧服务 - - 列出视频文件 - - 随机删除视频中的一帧 - - 批量处理 - - 元数据修改功能 - -- ✅ **AuthService**: 用户认证服务 - - 登录功能 - - 获取机器信息(机器名、用户名、IP) - -- ✅ **FileService**: 文件操作服务 - - 文件存在检查 - - 文件大小获取 - - 目录操作 - -- ✅ **日志系统**: 基于文件的日志记录 - -### 3. 前端功能实现 -- ✅ **主界面**: 侧边栏导航 + 主内容区 -- ✅ **VideoTab**: 视频拼接界面 - - 文件夹选择 - - 拼接模式选择 - - 数量设置 - - 审核图片选择 - - 结果展示 - -- ✅ **ExtractTab**: 视频抽帧界面 - - 文件夹选择 - - 抽帧数量设置 - - 批量处理 - - 元数据修改 - -- ✅ **LoginDialog**: 登录对话框 - -## 主要变化 - -### 后端变化 -1. **语言**: C# -> Go -2. **视频处理**: FFMpegCore -> FFmpeg 命令行调用 -3. **HTTP 客户端**: HttpClient -> Go net/http -4. **日志**: log4net -> 自定义日志系统 -5. **并发**: Task/async -> goroutine/channel - -### 前端变化 -1. **UI 框架**: WPF/XAML -> Vue3/HTML/CSS -2. **数据绑定**: WPF Binding -> Vue3 Reactive -3. **组件化**: WPF UserControl -> Vue Component - -## 需要注意的事项 - -### 1. Wails3 API 兼容性 -当前代码中使用了假设的 Wails3 API: -- `window.go.services.ServiceName` - 需要根据实际 Wails3 API 调整 -- 文件选择对话框 - 需要使用 Wails3 提供的文件对话框 API - -### 2. FFmpeg 路径 -- 确保 FFmpeg 已安装并在系统 PATH 中 -- 或者修改代码指定 FFmpeg 的完整路径 - -### 3. 文件路径处理 -- Windows 路径分隔符需要正确处理 -- 前端传递的路径格式需要与后端兼容 - -### 4. 进度更新 -- 当前实现是批量返回结果,不是实时进度 -- 如需实时进度,需要使用 Wails3 的事件系统 - -### 5. 错误处理 -- 需要完善错误处理和用户提示 -- 添加重试机制 - -## 待完善的功能 - -1. **文件选择对话框** - - 需要使用 Wails3 的原生文件对话框 API - - 当前使用 HTML5 API 作为临时方案 - -2. **进度实时更新** - - 使用 Wails3 事件系统实现实时进度 - - 或者使用 WebSocket/Server-Sent Events - -3. **错误处理** - - 更友好的错误提示 - - 错误日志记录 - -4. **UI 优化** - - 更现代化的界面设计 - - 响应式布局 - - 动画效果 - -5. **性能优化** - - 大文件处理优化 - - 内存使用优化 - - 并发控制优化 - -## 运行步骤 - -1. **安装依赖** - ```bash - # 安装 Wails3(参考 README.md) - # 安装前端依赖 - cd frontend - npm install - ``` - -2. **构建前端** - ```bash - npm run build - ``` - -3. **运行应用** - ```bash - cd .. - go run app.go - ``` - -## 测试建议 - -1. **功能测试** - - 视频拼接(两种模式) - - 视频抽帧 - - 元数据修改 - - 用户登录 - -2. **性能测试** - - 大量视频文件处理 - - 并发处理能力 - - 内存使用情况 - -3. **兼容性测试** - - 不同视频格式 - - 不同操作系统 - - 不同 FFmpeg 版本 - -## 已知问题 - -1. 文件选择在浏览器环境中受限,需要使用 Wails3 原生 API -2. 进度更新是批量返回,不是实时更新 -3. 错误处理需要进一步完善 -4. UI 需要根据实际 Wails3 API 调整 - -## 后续工作 - -1. 根据实际 Wails3 API 调整代码 -2. 实现实时进度更新 -3. 完善错误处理 -4. 优化 UI/UX -5. 添加单元测试 -6. 性能优化 -